第3篇 |基于 HarmonyOS 开发战旗小游戏项目引擎介绍

前言:

基于前面第二篇的介绍,这边来说下游戏引擎,这边采用的基本的逻辑编写,没有额外采用网络上的游戏引擎.

代码如下:

复制代码
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 算法流程
  1. 初始化访问标记矩阵 visited(9x8),将起始位置入队并标记已访问
  2. 从队列中取出一个节点,若非起始位置则加入可移动列表
  3. 若剩余移动力 > 0,向四个方向探索相邻格子
  4. 对每个相邻格子:检查有效性、是否已访问、移动消耗是否在剩余移动力范围内
  5. 若该格为空或有敌方单位(可移动过去攻击),则标记已访问并入队
  6. 重复步骤 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 攻击流程
  1. 验证目标位置存在敌方单位
  2. 检查目标是否在攻击范围内
  3. 根据伤害公式计算实际伤害
  4. 对目标调用 takeDamage() 扣减生命值
  5. 若目标阵亡,从地图上移除该单位(将 unitId 设为 null)
  6. 标记攻击者本回合已行动(hasActed = true)
  7. 返回 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设计和样式