前言:
基于前面第二篇的介绍,这边来说下游戏引擎,这边采用的基本的逻辑编写,没有额外采用网络上的游戏引擎.
代码如下:
import { Unit, UnitType, Faction, Position, MapTile, GameState, QueueItem, AttackResult } from '../common/GameModels'; import { MAP_WIDTH, MAP_HEIGHT, TerrainType, GamePhase } from '../common/GameConstants'; // 游戏引擎 - 处理战斗逻辑和地图操作 export class GameEngine { private gameState: GameState; constructor(gameState: GameState) { this.gameState = gameState; } // 初始化地图(9x8) initMap(): void { // 创建初始地图 - 远古帝国主题 const terrainMap: TerrainType[][] = [ [0, 0, 1, 1, 2, 1, 0, 0, 0], // 行0 [0, 1, 1, 0, 0, 0, 1, 0, 0], // 行1 [0, 0, 0, 0, 3, 0, 0, 0, 0], // 行2 (3=水域) [4, 4, 0, 0, 3, 0, 0, 1, 0], // 行3 (4=道路) [0, 0, 0, 0, 3, 0, 0, 0, 0], // 行4 [0, 1, 0, 0, 0, 0, 2, 1, 0], // 行5 [0, 0, 1, 1, 0, 1, 1, 0, 0], // 行6 [0, 0, 0, 0, 0, 0, 0, 0, 0], // 行7 ]; this.gameState.map = []; for (let y = 0; y < MAP_HEIGHT; y++) { this.gameState.map[y] = []; for (let x = 0; x < MAP_WIDTH; x++) { const tile: MapTile = { terrain: terrainMap[y][x], unitId: null }; this.gameState.map[y][x] = tile; } } } // 初始化单位 initUnits(): void { this.gameState.units = []; // 玩家单位(左下角) this.addUnit(0, UnitType.GENERAL, Faction.PLAYER, { x: 0, y: 7 }); this.addUnit(1, UnitType.WARRIOR, Faction.PLAYER, { x: 1, y: 7 }); this.addUnit(2, UnitType.WARRIOR, Faction.PLAYER, { x: 0, y: 6 }); this.addUnit(3, UnitType.ARCHER, Faction.PLAYER, { x: 2, y: 7 }); this.addUnit(4, UnitType.CAVALRY, Faction.PLAYER, { x: 1, y: 6 }); // 敌方单位(右上角) this.addUnit(5, UnitType.GENERAL, Faction.ENEMY, { x: 8, y: 0 }); this.addUnit(6, UnitType.WARRIOR, Faction.ENEMY, { x: 7, y: 0 }); this.addUnit(7, UnitType.WARRIOR, Faction.ENEMY, { x: 8, y: 1 }); this.addUnit(8, UnitType.ARCHER, Faction.ENEMY, { x: 6, y: 0 }); this.addUnit(9, UnitType.MAGE, Faction.ENEMY, { x: 7, y: 1 }); } // 添加单位到地图 private addUnit(id: number, type: UnitType, faction: Faction, pos: Position): void { const unit = new Unit(id, type, faction, pos); this.gameState.units.push(unit); this.gameState.map[pos.y][pos.x].unitId = id; } // 获取单位通过ID getUnitById(id: number): Unit | undefined { return this.gameState.units.find((u: Unit): boolean => u.id === id); } // 获取指定位置的单位 getUnitAt(pos: Position): Unit | undefined { if (!this.isValidPosition(pos)) return undefined; const tile = this.gameState.map[pos.y][pos.x]; if (tile.unitId === null) return undefined; return this.getUnitById(tile.unitId); } // 检查位置是否有效 isValidPosition(pos: Position): boolean { return pos.x >= 0 && pos.x < MAP_WIDTH && pos.y >= 0 && pos.y < MAP_HEIGHT; } // 检查位置是否为空 isPositionEmpty(pos: Position): boolean { if (!this.isValidPosition(pos)) return false; return this.gameState.map[pos.y][pos.x].unitId === null; } // 获取地形类型 getTerrainAt(pos: Position): TerrainType { if (!this.isValidPosition(pos)) return TerrainType.WATER; return this.gameState.map[pos.y][pos.x].terrain; } // 复制位置对象 private copyPosition(pos: Position): Position { return { x: pos.x, y: pos.y }; } // 计算可移动范围(BFS) getMovablePositions(unit: Unit): Position[] { if (unit.hasActed) return []; const movable: Position[] = []; const visited: boolean[][] = []; for (let y = 0; y < MAP_HEIGHT; y++) { visited[y] = []; for (let x = 0; x < MAP_WIDTH; x++) { visited[y][x] = false; } } const queue: QueueItem[] = []; const startItem: QueueItem = { pos: this.copyPosition(unit.position), remainingMove: unit.moveRange }; queue.push(startItem); visited[unit.position.y][unit.position.x] = true; while (queue.length > 0) { const current = queue.shift()!; // 添加当前位置到可移动列表(排除起始位置) if (current.pos.x !== unit.position.x || current.pos.y !== unit.position.y) { movable.push(this.copyPosition(current.pos)); } // 如果还有移动力,探索相邻格子 if (current.remainingMove > 0) { const directions: Position[] = [ { x: 0, y: -1 }, // 上 { x: 0, y: 1 }, // 下 { x: -1, y: 0 }, // 左 { x: 1, y: 0 } // 右 ]; for (let i = 0; i < directions.length; i++) { const dir = directions[i]; const newPos: Position = { x: current.pos.x + dir.x, y: current.pos.y + dir.y }; if (this.isValidPosition(newPos) && !visited[newPos.y][newPos.x]) { const terrain = this.getTerrainAt(newPos); const moveCost = unit.getMoveCost(terrain); if (moveCost <= current.remainingMove) { // 检查该位置是否为空或有一个敌方单位(可以攻击) const unitAtPos = this.getUnitAt(newPos); if (unitAtPos === undefined || unitAtPos.faction !== unit.faction) { visited[newPos.y][newPos.x] = true; const queueItem: QueueItem = { pos: newPos, remainingMove: current.remainingMove - moveCost }; queue.push(queueItem); } } } } } } return movable; } // 计算可攻击范围 getAttackablePositions(unit: Unit): Position[] { if (unit.hasActed) return []; const attackable: Position[] = []; const pos = unit.position; const range = unit.attackRange; // 根据攻击范围计算 for (let dy = -range; dy <= range; dy++) { for (let dx = -range; dx <= range; dx++) { // 对于近战单位(范围1),需要相邻;对于远程单位,需要在范围内 if (Math.abs(dx) + Math.abs(dy) > range) continue; if (dx === 0 && dy === 0) continue; // 跳过自己 const targetPos: Position = { x: pos.x + dx, y: pos.y + dy }; if (this.isValidPosition(targetPos)) { const targetUnit = this.getUnitAt(targetPos); // 只能攻击敌方单位 if (targetUnit && targetUnit.faction !== unit.faction && targetUnit.isAlive) { attackable.push(targetPos); } } } } return attackable; } // 移动单位 moveUnit(unit: Unit, targetPos: Position): boolean { // 检查目标位置是否在可移动范围内 const movable = this.getMovablePositions(unit); const canMove = movable.some((p: Position): boolean => p.x === targetPos.x && p.y === targetPos.y); if (!canMove) return false; // 检查目标位置是否为空 if (!this.isPositionEmpty(targetPos)) return false; // 更新地图 this.gameState.map[unit.position.y][unit.position.x].unitId = null; this.gameState.map[targetPos.y][targetPos.x].unitId = unit.id; // 更新单位位置 unit.position = targetPos; return true; } // 攻击单位 attackUnit(attacker: Unit, targetPos: Position): AttackResult { const targetUnit = this.getUnitAt(targetPos); if (!targetUnit || targetUnit.faction === attacker.faction) { const failResult: AttackResult = { success: false, damage: 0 }; return failResult; } // 检查是否在攻击范围内 const attackable = this.getAttackablePositions(attacker); const canAttack = attackable.some((p: Position): boolean => p.x === targetPos.x && p.y === targetPos.y); if (!canAttack) { const failResult: AttackResult = { success: false, damage: 0 }; return failResult; } // 计算伤害 const terrain = this.getTerrainAt(targetPos); const defenseBonus = targetUnit.getDefenseBonus(terrain); const actualDefense = targetUnit.defense * (1 + defenseBonus); const damage = Math.max(1, attacker.attack - actualDefense * 0.5); // 造成伤害 targetUnit.takeDamage(damage); // 如果目标单位死亡,从地图上移除 if (!targetUnit.isAlive) { this.gameState.map[targetPos.y][targetPos.x].unitId = null; } // 标记攻击者已行动 attacker.hasActed = true; const successResult: AttackResult = { success: true, damage: damage }; return successResult; } // 选择单位 selectUnit(unitId: number): void { this.gameState.selectedUnitId = unitId; const unit = this.getUnitById(unitId); if (unit) { this.gameState.movablePositions = this.getMovablePositions(unit); this.gameState.attackablePositions = this.getAttackablePositions(unit); } } // 取消选择 deselectUnit(): void { this.gameState.selectedUnitId = null; this.gameState.movablePositions = []; this.gameState.attackablePositions = []; } // 结束当前回合 endTurn(): GamePhase { if (this.gameState.currentPhase === GamePhase.PLAYER_TURN) { // 切换到敌方回合 this.gameState.currentPhase = GamePhase.ENEMY_TURN; // 重置敌方单位状态 this.gameState.units .filter((u: Unit): boolean => u.faction === Faction.ENEMY && u.isAlive) .forEach((u: Unit): void => u.resetTurn()); return GamePhase.ENEMY_TURN; } else { // 切换回玩家回合 this.gameState.turnCount++; this.gameState.currentPhase = GamePhase.PLAYER_TURN; // 重置玩家单位状态 this.gameState.units .filter((u: Unit): boolean => u.faction === Faction.PLAYER && u.isAlive) .forEach((u: Unit): void => u.resetTurn()); return GamePhase.PLAYER_TURN; } } // 检查胜利/失败条件 checkGameEnd(): GamePhase | null { const playerUnits = this.gameState.units.filter((u: Unit): boolean => u.faction === Faction.PLAYER && u.isAlive); const enemyUnits = this.gameState.units.filter((u: Unit): boolean => u.faction === Faction.ENEMY && u.isAlive); if (enemyUnits.length === 0) { return GamePhase.VICTORY; } if (playerUnits.length === 0) { return GamePhase.DEFEAT; } return null; // 游戏继续 } // 获取当前游戏状态 getGameState(): GameState { return this.gameState; } }
引擎介绍:
GameEngine 是一个基于 TypeScript 的回合制策略游戏核心引擎,采用面向对象设计,负责地图初始化、单位管理、移动与攻击逻辑、回合制控制以及胜负判定等核心功能。该引擎围绕远古帝国主题构建,在 9x8 的网格地图上实现双方对战的回合制战斗体验。
一:地图系统设计
这里只是做一个简单的,后期可以设计更大且环境和色彩更丰富的地图,等待后续完善
1 地图结构
地图为 9 列 x 8 行 的二维网格,每个格子(MapTile)包含两个字段:terrain(地形类型)和 unitId(占据该格的单位 ID,null 表示空格)。地图数据通过 terrainMap 硬编码初始化,随后逐格构建 MapTile 对象。
2 地形类型
地形类型对照表
| 枚举值 | 含义 | 战略影响 |
|---|---|---|
| 0 | 平原 | 基础地形,标准移动消耗 |
| 1 | 森林 | 可能增加移动消耗,提供防御加成 |
| 2 | 山地 | 高移动消耗,天然屏障 |
| 3 | 水域 | 不可通行(河流) |
| 4 | 道路 | 低移动消耗,快速通道 |
地图中央有一条纵向河流(列4,行2-4),将战场分割为左右两半,形成天然的战略走廊。行3的列0-1为道路,提供快速侧翼通道。这种布局鼓励玩家利用地形优势进行战术部署。
二:单位系统设计
游戏包含五种单位类型,每种具有不同的攻防属性和战术定位。单位属性由 Unit 类封装,包括攻击力、防御力、移动力、攻击范围等核心参数。
单位类型一览
| 类型 | 角色 | 特点 |
|---|---|---|
| GENERAL(将军) | 核心 | 指挥单位,阵亡可能导致特殊后果 |
| WARRIOR(战士) | 近战 | 高生命高防御,近战主力 |
| ARCHER(弓箭手) | 远程 | 远程攻击能力,可跨越一格攻击 |
| CAVALRY(骑兵) | 突击 | 高移动力,快速侧翼突击 |
| MAGE(法师) | 法术 | 远程法术攻击,高输出低防御 |
初始部署
双方各 5 个单位,分布在地图对角位置,形成对称开局:
- 玩家阵营(蓝方): 部署在左下角(行6-7,列0-2),含1将军+2战士+1弓箭手+1骑兵
- 敌方阵营(红方): 部署在右上角(行0-1,列6-8),含1将军+2战士+1弓箭手+1法师
- 双方初始单位 ID 分别为 0-4 和 5-9,通过 Faction 枚举区分阵营归属
三:移动系统设计
BFS 寻路算法
getMovablePositions() 方法使用广度优先搜索(BFS)计算单位可达的所有位置。算法从单位当前位置出发,在剩余移动力范围内向四个方向(上下左右)逐步扩展,根据地形移动消耗扣减移动力,直到所有可达位置被遍历完毕。
1 算法流程
- 初始化访问标记矩阵 visited(9x8),将起始位置入队并标记已访问
- 从队列中取出一个节点,若非起始位置则加入可移动列表
- 若剩余移动力 > 0,向四个方向探索相邻格子
- 对每个相邻格子:检查有效性、是否已访问、移动消耗是否在剩余移动力范围内
- 若该格为空或有敌方单位(可移动过去攻击),则标记已访问并入队
- 重复步骤 2-5 直到队列为空
2 关键设计决策
- 使用 visited 矩阵避免重复访问,确保每个格子最多被处理一次
- 友方单位所在格子不可通过(阻挡移动),敌方单位所在格子可进入(触发攻击)
- 起始位置本身不包含在可移动列表中
- 移动力通过 unit.getMoveCost(terrain) 方法根据地形类型动态计算消耗
四:战斗系统设计
1 攻击范围计算
getAttackablePositions() 使用曼哈顿距离模型计算可攻击范围。从单位当前位置出发,在攻击范围(attackRange)内的所有有效坐标中,筛选出存在存活敌方单位的目标位置。近战单位攻击范围为1(仅相邻格),远程单位攻击范围更大。
2 伤害计算公式
defenseBonus = targetUnit.getDefenseBonus(terrain) actualDefense = targetUnit.defense * (1 + defenseBonus) damage = max(1, attacker.attack - actualDefense * 0.5)
伤害计算考虑三个因素:攻击者攻击力、目标防御力、地形防御加成。最终伤害至少为1,确保任何攻击都能造成有效伤害。地形防御加成通过乘法叠加,使得森林和山地中的单位更加耐打。
3 攻击流程
- 验证目标位置存在敌方单位
- 检查目标是否在攻击范围内
- 根据伤害公式计算实际伤害
- 对目标调用 takeDamage() 扣减生命值
- 若目标阵亡,从地图上移除该单位(将 unitId 设为 null)
- 标记攻击者本回合已行动(hasActed = true)
- 返回 AttackResult 结构,包含成功标志和伤害数值
五:回合制系统设计
1 回合流程
游戏采用交替回合制,由 GamePhase 枚举控制当前阶段。endTurn() 方法负责在玩家回合与敌方回合之间切换,并重置对应阵营单位的行动状态。
回合切换逻辑
| 当前阶段 | 切换后阶段 | 重置单位 |
|---|---|---|
| PLAYER_TURN | ENEMY_TURN | 所有存活敌方单位 resetTurn() |
| ENEMY_TURN | PLAYER_TURN | 所有存活玩家单位 resetTurn(),回合数+1 |
2 行动控制
每个单位通过 hasActed 标记控制本回合是否已行动。已行动的单位不可再移动或攻击,getMovablePositions() 和 getAttackablePositions() 对已行动单位直接返回空数组。resetTurn() 方法在回合切换时将 hasActed 重置为 false。
3 选单位与缓存
selectUnit() 方法在选中单位时,同时计算并缓存该单位的可移动位置和可攻击位置到 GameState 中,供 UI 层直接读取渲染。deselectUnit() 清除选中和缓存数据。这种缓存策略避免了 UI 频繁调用计算方法的开销。
六:胜负判定设计
checkGameEnd() 方法在每次行动后调用,检查双方存活单位数量:
- 若敌方所有单位阵亡 -> 返回 GamePhase.VICTORY(玩家胜利)
- 若玩家所有单位阵亡 -> 返回 GamePhase.DEFEAT(玩家失败)
- 双方仍有存活单位 -> 返回 null(游戏继续)
最终实现效果:
后期会加上任务和背景的图片,可能效果更好看些,目前简单版就如此,请见谅!

接下来任务:
1:继续介绍下其余页面和实现
2:完善小游戏ux设计和样式