战斗更新
简单关卡配置
首先,我们要先配置敌人和干员的配置,暂时固定。敌人行动路线的拐点(enemyPath)暂时也固定。
// Vec2 世界坐标,1 单位 = 1 格
type Vec2 struct {
X, Y float64
}
const (
mapWidth = 9 // 地图宽(格)
mapHeight = 2 // 地图高(格)
pathY = 1.0 // 敌人路径所在的行(第 2 行),干员部署在第 1 行
)
const (
enemyConfigID = 1 // 敌人配置ID
enemyMaxHP = 100 // 敌人血量
enemySpeed = 1.2 // 敌人速度(格/秒)
baseMaxHP = 1 // 基地生命,漏一只怪即失败
operatorMaxHP = 100 // 干员血量(当前不会被攻击,仅展示)
attackRange = 2.5 // 干员攻击半径(格)
attackDamage = 12 // 干员每帧伤害
deployCost = 4 // 部署干员消耗
initCost = 10 // 开局费用
maxOperators = 1 // 场上干员上限
)
// enemyPath 敌人行进路线:第 2 行从左到右的一条直线
var enemyPath = []Vec2{{0, pathY}, {mapWidth - 1, pathY}}
房间与战斗推进
定义房间的基本数据结构,
type Room struct {
ID uint32
Seed uint64
connID uint64
send func(msgID uint16, body []byte) // 推送给房间所属客户端
mu sync.Mutex
frame uint32//已进行的帧数
cost int32
baseHP int32
operators []*Operator
enemies []*Enemy
over bool
startedAt time.Time
done chan struct{}
}
然后我们定义一个新建房间的函数。传入connID、send的函数
id := roomSeq.Add(1)//id自增
Seed: uint64(time.Now().UnixNano()),//利用系统时间创建随机种子
并将房间id传入房间组
rooms[id] = r
还有获取房间号,移除房间,这里就不展开说了。
我们使用run函数进行tick循环,一秒五帧。
ticker := time.NewTicker(tickInterval)
...
case <-ticker.C:
if r.step(dt) {//返回true表示结束
return
}
接着,我们要对每一帧进行推进
先计算路径总长
func pathLength() float64 {
total := 0.0
for i := 1; i < len(enemyPath); i++ {
a, b := enemyPath[i-1], enemyPath[i]
total += math.Hypot(b.X-a.X, b.Y-a.Y)
}
return total
}
敌人前进的路程=敌人速度*tickInterval
for _, e := range r.enemies {//敌人的累积行程
e.Dist += enemySpeed * dt
}
干员是否对敌人造成伤害检测
for _, op := range r.operators {
for _, e := range r.enemies {
if e.HP <= 0 {
continue
}
p := pointAtDistance(e.Dist)//pointAtDistance 按已行进距离求路径上的坐标;超出总长时返回终点
if math.Hypot(p.X-float64(op.X), p.Y-float64(op.Y)) <= attackRange {
e.HP -= attackDamage
}
}
}
其中pointAtDistance的函数是这样的,取相邻两段路径拐点,并计算距离。如果敌人已经前进的路程小于等于当前相邻拐点的距离,则计算当前坐标。如果大于,则减去相邻拐点的距离,继续检测下一个相邻拐点。
func pointAtDistance(d float64) Vec2 {
for i := 1; i < len(enemyPath); i++ {
a, b := enemyPath[i-1], enemyPath[i]
seg := math.Hypot(b.X-a.X, b.Y-a.Y)
if seg == 0 {
return a
}
if d <= seg {
t := d / seg
return Vec2{X: a.X + (b.X-a.X)*t, Y: a.Y + (b.Y-a.Y)*t}
}
d -= seg
}
return enemyPath[len(enemyPath)-1]
}
移除阵亡敌人;走到终点的敌人扣基地生命
alive := make([]*Enemy, 0, len(r.enemies))
for _, e := range r.enemies {
if e.HP <= 0 {
continue
}
if e.Dist >= total {
r.baseHP--
logger.Printf("房间%d 敌人到达终点,基地生命%d", r.ID, r.baseHP)
continue
}
alive = append(alive, e)
}
r.enemies = alive
胜负判定
result := int32(-1)
switch {
case r.baseHP <= 0:
result = resultLose
case len(r.enemies) == 0:
result = resultWin
}
state := r.snapshotLocked()
if result >= 0 {
r.over = true
}
r.mu.Unlock()//先解锁再推送,避免慢客户端阻塞 tick
r.pushState(state)//下帧数状态
if result >= 0 {//最终结算
r.pushSettle(result)
return true
}
return false
snapshotLocked 生成当前帧状态快照,调用方需持有 r.mu
func (r *Room) snapshotLocked() *api.BattleStatePush {
s := &api.BattleStatePush{//房间状态
RoomId: r.ID,
Frame: r.frame,
Cost: r.cost,
BaseHp: r.baseHP,
}
for _, op := range r.operators {//干员状态
s.Operators = append(s.Operators, &api.OperatorState{
OperatorId: op.ID,
GridX: op.X,
GridY: op.Y,
Hp: operatorMaxHP,
Direction: op.Direction,
})
}
for _, e := range r.enemies {//敌人状态
p := pointAtDistance(e.Dist)
s.Enemies = append(s.Enemies, &api.EnemyState{
EnemyId: enemyConfigID,
InstanceId: e.InstanceID,
Hp: e.HP,
PosX: float32(p.X),
PosY: float32(p.Y),
})
}
return s
}
pushState 下发帧状态(msgId 2004)给客户端
func (r *Room) pushState(state *api.BattleStatePush) {
body, err := proto.Marshal(state)
if err != nil {
logger.Println("序列化 BattleStatePush 失败:", err)
return
}
r.send(2004, body)
}
结束后pushSettle 下发结算(msgId 2005),随后销毁房间
func (r *Room) pushSettle(result int32) {
duration := int32(time.Since(r.startedAt).Seconds())
score := int32(0)
var rewards []string
if result == resultWin {
score = 1000
rewards = []string{"exp:100", "gold:500"}
}
body, err := proto.Marshal(&api.BattleSettlePush{
RoomId: r.ID,
Result: result,
Score: score,
DurationSec: duration,
Rewards: rewards,
})
if err != nil {
logger.Println("序列化 BattleSettlePush 失败:", err)
return
}
r.send(2005, body)
logger.Printf("房间%d 战斗结束:result=%d 用时%ds 分数%d", r.ID, result, duration, score)
removeRoom(r.ID)
}
战斗服务
监听网关连接,处理 2000+ 战斗消息。这个就比战斗房间那个简单多了,而且这个也与网关和登陆的服务类似,所以没有花太多时间。
func handleGatewayConn(conn net.Conn) {
....
case 2000:
respMsgID, resp = 2001, handleEnterRoom(p.Payload, p.ConnId, cw)
case 2002:
respMsgID, resp = 2003, handleDeployOperator(p.Payload)
....
}
2000就是处理创建房间的请求,2002是处理放置干员的请求。这个就是调用函数的事情了,大头在战斗房间那章说过,不多赘述。
这就是我这几天的成果,我花了很多时间去设想战斗房间与角色、敌人检定的事情。有点头疼。还是先去玩月下夜想曲休息一下把~
我是Strandingwhale,也可以叫我bridges,计算机专业,专注游戏后端与Unreal引擎开发。在git pages和博客园上发布文章,其他网站上的都不是本人。
目前还在上大学,希望能做出很牛逼的项目,然后去到我喜欢的岗位。
本人是冯·诺伊曼派,不是图灵学派。
古法编程,AI提供一点思路,少用AI。
喜欢玩明日方舟
喜欢的游戏还有 死亡搁浅 灵魂摆渡人 异星工厂 等
浙公网安备 33010602011771号