战斗更新

简单关卡配置

首先,我们要先配置敌人和干员的配置,暂时固定。敌人行动路线的拐点(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是处理放置干员的请求。这个就是调用函数的事情了,大头在战斗房间那章说过,不多赘述。

这就是我这几天的成果,我花了很多时间去设想战斗房间与角色、敌人检定的事情。有点头疼。还是先去玩月下夜想曲休息一下把~

posted @ 2026-10-30 23:47  布里吉斯  阅读(11)  评论(0)    收藏  举报