前言:

      游戏主界面GamePage.ets介绍,主要采用Canvas 2D 渲染系统--绘制地图、单位与血条

代码如下:

import router from '@ohos.router';
import { GameEngine } from '../engine/GameEngine';
import { AIEngine } from '../ai/AIEngine';
import { Unit, Faction, Position, GameState } from '../common/GameModels';
import { MAP_WIDTH, MAP_HEIGHT, TILE_SIZE, TerrainType, UnitType, GamePhase,
  TERRAIN_COLORS, TERRAIN_NAMES, UNIT_NAMES,
  UNIT_COLOR_PLAYER, UNIT_COLOR_ENEMY, SELECTED_COLOR,
  MOVABLE_COLOR, ATTACKABLE_COLOR } from '../common/GameConstants';

@Entry
@Component
struct GamePage {
  // 游戏状态
  private gameEngine: GameEngine = new GameEngine(new GameState());
  private aiEngine: AIEngine = new AIEngine(this.gameEngine);

  @State gameState: GameState = this.gameEngine.getGameState();
  @State isAITurn: boolean = false;
  @State gameMessage: string = '我方回合 - 请选择单位';
  @State showRules: boolean = false;

  // Canvas上下文
  private settings: RenderingContextSettings = new RenderingContextSettings(true);
  private ctx: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);

  aboutToAppear() {
    // 初始化游戏
    this.gameEngine.initMap();
    this.gameEngine.initUnits();
    this.gameState = this.gameEngine.getGameState();
  }

  build() {
    Column() {
      // 顶部信息栏
      this.buildTopBar()

      // 游戏地图(Canvas)
      Canvas(this.ctx)
        .width(MAP_WIDTH * TILE_SIZE)
        .height(MAP_HEIGHT * TILE_SIZE)
        .backgroundColor('#87CEEB')
        .onReady(() => {
          this.drawGameMap();
        })
        .onTouch((event: TouchEvent) => {
          this.handleMapTouch(event);
        })
        .margin(10)

      // 底部操作栏
      this.buildBottomBar()

      // 回合信息
      this.buildTurnInfo()

      // 游戏规则弹窗
      if (this.showRules) {
        this.buildRulesDialog()
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5DEB3')
    .padding(10)
  }

  // 顶部信息栏
  @Builder
  buildTopBar() {
    Row() {
      Text('远古帝国战旗')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .fontColor('#8B4513')

      Blank()

      Button('返回')
        .height(30)
        .backgroundColor('#A0522D')
        .onClick(() => {
          router.back();
        })
    }
    .width('100%')
    .margin({ bottom: 10 })
  }

  // 底部操作栏
  @Builder
  buildBottomBar() {
    Row({ space: 10 }) {
      Button('结束回合')
        .height(40)
        .backgroundColor('#8B4513')
        .enabled(!this.isAITurn && this.gameState.currentPhase === GamePhase.PLAYER_TURN)
        .onClick(() => {
          this.endPlayerTurn();
        })

      Button('游戏规则')
        .height(40)
        .backgroundColor('#A0522D')
        .onClick(() => {
          this.showRules = true;
        })
    }
    .margin({ top: 10, bottom: 10 })
  }

  // 回合信息
  @Builder
  buildTurnInfo() {
    Column() {
      Text(this.gameMessage)
        .fontSize(16)
        .fontColor(this.isAITurn ? '#DC143C' : '#4169E1')
        .textAlign(TextAlign.Center)

      Text(`回合: ${this.gameState.turnCount}`)
        .fontSize(14)
        .fontColor('#666666')
        .margin({ top: 5 })
    }
    .width('100%')
    .padding(10)
    .backgroundColor('#FFF8DC')
    .borderRadius(8)
  }

  // 游戏规则弹窗
  @Builder
  buildRulesDialog() {
    Column() {
      Text('游戏规则')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 15 })

      Text('1. 这是一款回合制战旗游戏')
        .fontSize(14)
        .margin({ bottom: 5 })

      Text('2. 点击我方单位选中,绿色格子可移动')
        .fontSize(14)
        .margin({ bottom: 5 })

      Text('3. 红色格子可攻击敌方单位')
        .fontSize(14)
        .margin({ bottom: 5 })

      Text('4. 每个单位每回合可以移动和攻击一次')
        .fontSize(14)
        .margin({ bottom: 5 })

      Text('5. 消灭所有敌方单位获得胜利')
        .fontSize(14)
        .margin({ bottom: 15 })

      Text('兵种介绍:')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 5 })

      Text('战士:均衡型,适合前线作战')
        .fontSize(13)
        .margin({ bottom: 3 })

      Text('弓箭手:远程攻击,脆弱但输出高')
        .fontSize(13)
        .margin({ bottom: 3 })

      Text('骑兵:高机动,适合快速突袭')
        .fontSize(13)
        .margin({ bottom: 3 })

      Text('法师:魔法攻击,范围伤害')
        .fontSize(13)
        .margin({ bottom: 3 })

      Text('将军:高属性,率领部队')
        .fontSize(13)
        .margin({ bottom: 15 })

      Button('关闭')
        .width(100)
        .height(35)
        .backgroundColor('#8B4513')
        .onClick(() => {
          this.showRules = false;
        })
    }
    .width('80%')
    .padding(20)
    .backgroundColor('#FFFFFF')
    .borderRadius(12)
    .position({ x: '10%', y: '20%' })
    .zIndex(999)
  }

  // 绘制游戏地图
  drawGameMap(): void {
    const ctx = this.ctx;
    const state = this.gameState;

    // 清空画布
    ctx.clearRect(0, 0, MAP_WIDTH * TILE_SIZE, MAP_HEIGHT * TILE_SIZE);

    // 绘制地形
    for (let y = 0; y < MAP_HEIGHT; y++) {
      for (let x = 0; x < MAP_WIDTH; x++) {
        const terrain = state.map[y][x].terrain;
        const color = TERRAIN_COLORS[terrain];

        // 绘制格子
        ctx.fillStyle = color;
        ctx.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE);

        // 绘制网格线
        ctx.strokeStyle = '#000000';
        ctx.lineWidth = 1;
        ctx.strokeRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE);

        // 绘制地形符号
        this.drawTerrainSymbol(ctx, x, y, terrain);
      }
    }

    // 绘制可移动范围
    if (state.selectedUnitId !== null) {
      for (const pos of state.movablePositions) {
        ctx.fillStyle = MOVABLE_COLOR + '80'; // 半透明
        ctx.fillRect(pos.x * TILE_SIZE, pos.y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
      }

      // 绘制可攻击范围
      for (const pos of state.attackablePositions) {
        ctx.fillStyle = ATTACKABLE_COLOR + '80';
        ctx.fillRect(pos.x * TILE_SIZE, pos.y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
      }
    }

    // 绘制单位
    for (const unit of state.units) {
      if (!unit.isAlive) continue;
      this.drawUnit(ctx, unit);
    }

    // 绘制选中效果
    if (state.selectedUnitId !== null) {
      const unit = this.gameEngine.getUnitById(state.selectedUnitId);
      if (unit) {
        ctx.strokeStyle = SELECTED_COLOR;
        ctx.lineWidth = 3;
        ctx.strokeRect(
          unit.position.x * TILE_SIZE + 2,
          unit.position.y * TILE_SIZE + 2,
          TILE_SIZE - 4,
          TILE_SIZE - 4
        );
      }
    }
  }

  // 绘制地形符号
  private drawTerrainSymbol(ctx: CanvasRenderingContext2D, x: number, y: number, terrain: TerrainType): void {
    const cx = x * TILE_SIZE + TILE_SIZE / 2;
    const cy = y * TILE_SIZE + TILE_SIZE / 2;

    ctx.font = '12px Arial';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillStyle = '#000000';

    switch (terrain) {
      case TerrainType.FOREST:
        ctx.fillText('🌲', cx, cy);
        break;
      case TerrainType.MOUNTAIN:
        ctx.fillText('⛰️', cx, cy);
        break;
      case TerrainType.WATER:
        ctx.fillText('💧', cx, cy);
        break;
      case TerrainType.ROAD:
        ctx.fillText('🛤️', cx, cy);
        break;
    }
  }

  // 绘制单位
  private drawUnit(ctx: CanvasRenderingContext2D, unit: Unit): void {
    const x = unit.position.x * TILE_SIZE;
    const y = unit.position.y * TILE_SIZE;
    const cx = x + TILE_SIZE / 2;
    const cy = y + TILE_SIZE / 2;

    // 绘制单位背景(圆形)
    ctx.beginPath();
    ctx.arc(cx, cy, TILE_SIZE / 2 - 8, 0, Math.PI * 2);
    ctx.fillStyle = unit.faction === Faction.PLAYER ? UNIT_COLOR_PLAYER : UNIT_COLOR_ENEMY;
    ctx.fill();
    ctx.strokeStyle = '#000000';
    ctx.lineWidth = 2;
    ctx.stroke();

    // 绘制单位符号
    ctx.font = '20px Arial';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';

    const symbols = ['⚔️', '🏹', '🐎', '🔮', '👑'];
    ctx.fillText(symbols[unit.type], cx, cy - 5);

    // 绘制血条
    const barWidth = TILE_SIZE - 16;
    const barHeight = 4;
    const barX = x + 8;
    const barY = y + TILE_SIZE - 10;

    // 背景
    ctx.fillStyle = '#000000';
    ctx.fillRect(barX, barY, barWidth, barHeight);

    // 当前血量
    const hpPercent = unit.hp / unit.maxHp;
    ctx.fillStyle = hpPercent > 0.5 ? '#00FF00' : hpPercent > 0.25 ? '#FFFF00' : '#FF0000';
    ctx.fillRect(barX, barY, barWidth * hpPercent, barHeight);

    // 绘制血条边框
    ctx.strokeStyle = '#000000';
    ctx.lineWidth = 1;
    ctx.strokeRect(barX, barY, barWidth, barHeight);
  }

  // 处理地图触摸事件
  handleMapTouch(event: TouchEvent): void {
    if (this.isAITurn) return;
    if (this.gameState.currentPhase !== GamePhase.PLAYER_TURN) return;

    const touch = event.touches[0];
    if (!touch) return;

    // 计算点击的格子坐标
    const rect = touch;
    const x = Math.floor(rect.x / TILE_SIZE);
    const y = Math.floor(rect.y / TILE_SIZE);

    if (!this.gameEngine.isValidPosition({ x, y })) return;

    const clickedUnit = this.gameEngine.getUnitAt({ x, y });

    // 如果有单位被选中
    if (this.gameState.selectedUnitId !== null) {
      const selectedUnit = this.gameEngine.getUnitById(this.gameState.selectedUnitId);

      if (!selectedUnit) {
        this.gameEngine.deselectUnit();
        this.gameState = this.gameEngine.getGameState();
        this.drawGameMap();
        return;
      }

      // 点击可移动格子
      const isMovable = this.gameState.movablePositions.some(p => p.x === x && p.y === y);
      if (isMovable) {
        const success = this.gameEngine.moveUnit(selectedUnit, { x, y });
        if (success) {
          this.gameEngine.deselectUnit();
          this.gameState = this.gameEngine.getGameState();
          this.drawGameMap();
          this.checkGameEnd();
          return;
        }
      }

      // 点击可攻击格子
      const isAttackable = this.gameState.attackablePositions.some(p => p.x === x && p.y === y);
      if (isAttackable) {
        const result = this.gameEngine.attackUnit(selectedUnit, { x, y });
        if (result.success) {
          this.gameMessage = `攻击造成 ${Math.floor(result.damage)} 点伤害!`;
          this.gameEngine.deselectUnit();
          this.gameState = this.gameEngine.getGameState();
          this.drawGameMap();
          this.checkGameEnd();
          return;
        }
      }

      // 点击其他我方单位 - 切换选择
      if (clickedUnit && clickedUnit.faction === Faction.PLAYER && !clickedUnit.hasActed) {
        this.gameEngine.selectUnit(clickedUnit.id);
        this.gameState = this.gameEngine.getGameState();
        this.drawGameMap();
        return;
      }

      // 点击空白或其他 - 取消选择
      this.gameEngine.deselectUnit();
      this.gameState = this.gameEngine.getGameState();
      this.drawGameMap();
    } else {
      // 没有单位被选中,尝试选择我方单位
      if (clickedUnit && clickedUnit.faction === Faction.PLAYER && !clickedUnit.hasActed) {
        this.gameEngine.selectUnit(clickedUnit.id);
        this.gameState = this.gameEngine.getGameState();
        this.drawGameMap();
        this.gameMessage = `选中${UNIT_NAMES[clickedUnit.type]},绿色可移动,红色可攻击`;
      }
    }
  }

  // 结束玩家回合
  endPlayerTurn(): void {
    this.gameEngine.deselectUnit();
    this.gameEngine.endTurn();
    this.gameState = this.gameEngine.getGameState();
    this.isAITurn = true;
    this.gameMessage = '敌方回合...';
    this.drawGameMap();

    // 执行AI回合
    setTimeout(() => {
      this.executeAITurn();
    }, 500);
  }

  // 执行AI回合
  executeAITurn(): void {
    this.aiEngine.executeTurn().then(() => {
      // AI回合结束,切换回玩家回合
      this.gameEngine.endTurn();
      this.gameState = this.gameEngine.getGameState();
      this.isAITurn = false;
      this.gameMessage = '我方回合 - 请选择单位';
      this.drawGameMap();
      this.checkGameEnd();
    });
  }

  // 检查游戏是否结束
  checkGameEnd(): void {
    const result = this.gameEngine.checkGameEnd();
    if (result === GamePhase.VICTORY) {
      this.gameMessage = '🎉 胜利!你消灭了所有敌人!';
      this.showGameEndDialog(true);
    } else if (result === GamePhase.DEFEAT) {
      this.gameMessage = '💀 失败!你的部队被全歼了...';
      this.showGameEndDialog(false);
    }
  }

  // 显示游戏结束对话框
  showGameEndDialog(isVictory: boolean): void {
    // 这里可以实现一个更美观的对话框
    // 暂时使用简单的提示
    setTimeout(() => {
      if (isVictory) {
        this.gameMessage = '🎉 胜利!点击返回重新开始';
      } else {
        this.gameMessage = '💀 失败!点击返回重新开始';
      }
    }, 1000);
  }
}

一、为什么选择 Canvas

ArkUI 提供了两种游戏画面实现方案:

| 方案 | 优点 | 缺点 |
|------|------|------|
| **ArkUI 组件布局** | 声明式、自动刷新 | 性能差,复杂动画困难 |
| **Canvas 2D API** | 精确控制、性能好 | 命令式编程,需手动刷新 |

战棋游戏的地图是动态的(高亮格子随选择变化),需要频繁重绘,Canvas 2D 是更合适的选择。

二、Canvas 初始化

```
// GamePage.ets
private settings: RenderingContextSettings = new RenderingContextSettings(true); // true = 开启抗锯齿
private ctx: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
```

在 `build()` 方法中嵌入 Canvas 组件:

```
Canvas(this.ctx)
  .width(MAP_WIDTH * TILE_SIZE)    // 9 × 64 = 576px
  .height(MAP_HEIGHT * TILE_SIZE)  // 8 × 64 = 512px
  .backgroundColor('#87CEEB')      // 天蓝色背景(万一有空白区域)
  .onReady(() => {
    this.drawGameMap();            // Canvas 准备好后立即绘制
  })
  .onTouch((event: TouchEvent) => {
    this.handleMapTouch(event);    // 触摸事件处理
  })
  .margin(10)
```

**`onReady` 回调**:Canvas 组件挂载到 DOM 后触发,确保在真正可以绘制之前不执行任何绘制操作。

三、主渲染函数:drawGameMap

```
drawGameMap(): void {
  const ctx = this.ctx;
  const state = this.gameState;

  // 第1步:清空画布
  ctx.clearRect(0, 0, MAP_WIDTH * TILE_SIZE, MAP_HEIGHT * TILE_SIZE);

  // 第2步:绘制地形层
  for (let y = 0; y < MAP_HEIGHT; y++) {
    for (let x = 0; x < MAP_WIDTH; x++) {
      const terrain = state.map[y][x].terrain;
      // 绘制填色格子
      ctx.fillStyle = TERRAIN_COLORS[terrain];
      ctx.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
      // 绘制网格线
      ctx.strokeStyle = '#000000';
      ctx.lineWidth = 1;
      ctx.strokeRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
      // 绘制地形 Emoji 符号
      this.drawTerrainSymbol(ctx, x, y, terrain);
    }
  }

  // 第3步:绘制高亮层(可移动/攻击范围)
  if (state.selectedUnitId !== null) {
    for (const pos of state.movablePositions) {
      ctx.fillStyle = MOVABLE_COLOR + '80';    // '#7CFC0080' 半透明
      ctx.fillRect(pos.x * TILE_SIZE, pos.y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
    }
    for (const pos of state.attackablePositions) {
      ctx.fillStyle = ATTACKABLE_COLOR + '80'; // '#FF634780' 半透明
      ctx.fillRect(pos.x * TILE_SIZE, pos.y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
    }
  }

  // 第4步:绘制单位层
  for (const unit of state.units) {
    if (!unit.isAlive) continue; // 跳过死亡单位
    this.drawUnit(ctx, unit);
  }

  // 第5步:绘制选中边框(最上层)
  if (state.selectedUnitId !== null) {
    const unit = this.gameEngine.getUnitById(state.selectedUnitId);
    if (unit) {
      ctx.strokeStyle = SELECTED_COLOR; // '#FFD700' 金色
      ctx.lineWidth = 3;
      ctx.strokeRect(
        unit.position.x * TILE_SIZE + 2,
        unit.position.y * TILE_SIZE + 2,
        TILE_SIZE - 4,
        TILE_SIZE - 4
      );
    }
  }
}
```

分层绘制是关键

Canvas 是**画家算法**(Painter's Algorithm):后画的覆盖先画的。绘制顺序决定了视觉层次:

```
地形层(最底层)
  ↓ 覆盖
高亮层(半透明遮罩)
  ↓ 覆盖
单位层(圆形 + Emoji)
  ↓ 覆盖
选中边框(最顶层)
```

四、地形符号绘制

```
private drawTerrainSymbol(
  ctx: CanvasRenderingContext2D,
  x: number, y: number,
  terrain: TerrainType
): void {
  const cx = x * TILE_SIZE + TILE_SIZE / 2; // 格子中心 X
  const cy = y * TILE_SIZE + TILE_SIZE / 2; // 格子中心 Y

  ctx.font = '12px Arial';
  ctx.textAlign = 'center';
  ctx.textBaseline = 'middle';
  ctx.fillStyle = '#000000';

  switch (terrain) {
    case TerrainType.FOREST:   ctx.fillText('🌲', cx, cy); break;
    case TerrainType.MOUNTAIN: ctx.fillText('⛰️', cx, cy); break;
    case TerrainType.WATER:    ctx.fillText('💧', cx, cy); break;
    case TerrainType.ROAD:     ctx.fillText('🛤️', cx, cy); break;
    // 平原不绘制符号(保持简洁)
  }
}
```

知识点:Canvas 文本对齐

`textAlign = 'center'` + `textBaseline = 'middle'` 组合,使文本以 `(cx, cy)` 为中心点绘制,这是在格子中居中显示 Emoji 的标准做法。

Emoji 在 Canvas 中的限制:

不同设备上 Emoji 的渲染尺寸和位置可能略有差异。`font = '12px Arial'` 设置了字号,但 Emoji 的实际尺寸由系统字体决定。在生产项目中,建议使用精灵图(Sprite Sheet)替代 Emoji 以确保跨设备一致性。

五、单位绘制:圆形 + Emoji + 血条

```
private drawUnit(ctx: CanvasRenderingContext2D, unit: Unit): void {
  const x = unit.position.x * TILE_SIZE;
  const y = unit.position.y * TILE_SIZE;
  const cx = x + TILE_SIZE / 2; // 格子中心 X
  const cy = y + TILE_SIZE / 2; // 格子中心 Y

  // === 绘制圆形背景 ===
  ctx.beginPath();
  ctx.arc(cx, cy, TILE_SIZE / 2 - 8, 0, Math.PI * 2); // 半径 = 64/2 - 8 = 24px
  ctx.fillStyle = unit.faction === Faction.PLAYER ? UNIT_COLOR_PLAYER : UNIT_COLOR_ENEMY;
  ctx.fill();
  ctx.strokeStyle = '#000000';
  ctx.lineWidth = 2;
  ctx.stroke();

  // === 绘制兵种 Emoji ===
  ctx.font = '20px Arial';
  ctx.textAlign = 'center';
  ctx.textBaseline = 'middle';
  const symbols = ['⚔️', '🏹', '🐎', '🔮', '👑'];
  ctx.fillText(symbols[unit.type], cx, cy - 5); // 略微上移给血条留空间

  // === 绘制血条 ===
  const barWidth  = TILE_SIZE - 16; // 48px 宽
  const barHeight = 4;
  const barX = x + 8;
  const barY = y + TILE_SIZE - 10; // 格子底部

  // 血条背景(黑色)
  ctx.fillStyle = '#000000';
  ctx.fillRect(barX, barY, barWidth, barHeight);

  // 当前血量(颜色随血量变化)
  const hpPercent = unit.hp / unit.maxHp;
  ctx.fillStyle = hpPercent > 0.5 ? '#00FF00'   // 绿色:血量充足(>50%)
               : hpPercent > 0.25 ? '#FFFF00'   // 黄色:血量减少(25%-50%)
                                  : '#FF0000';   // 红色:血量危险(<25%)
  ctx.fillRect(barX, barY, barWidth * hpPercent, barHeight);

  // 血条边框
  ctx.strokeStyle = '#000000';
  ctx.lineWidth = 1;
  ctx.strokeRect(barX, barY, barWidth, barHeight);
}
```

血条颜色三段式设计

```
ctx.fillStyle = hpPercent > 0.5  ? '#00FF00'
              : hpPercent > 0.25 ? '#FFFF00'
                                 : '#FF0000';
```

这是三元运算符的嵌套使用,实现了三段式血条颜色:

```
HP > 50%  → 绿色(安全)
HP 25-50% → 黄色(警戒)
HP < 25%  → 红色(危险)
```

这个设计来自大多数游戏的 UI 惯例,玩家无需读数字就能快速判断单位状态。

Canvas arc 方法

```
ctx.arc(cx, cy, radius, startAngle, endAngle)
// cx, cy: 圆心坐标
// radius: 半径
// startAngle: 起始角度(弧度)
// endAngle: 结束角度(弧度)
// 完整圆: 0 到 Math.PI * 2
```

`ctx.beginPath()` 必须在绘制圆弧前调用,用于开始一条新路径,否则多个圆形会连接在一起。

六、选中高亮效果

```
// 金色描边框,内缩2px避免被网格线遮挡
ctx.strokeStyle = SELECTED_COLOR; // '#FFD700' 金色
ctx.lineWidth = 3;
ctx.strokeRect(
  unit.position.x * TILE_SIZE + 2,  // +2px 内缩
  unit.position.y * TILE_SIZE + 2,
  TILE_SIZE - 4,                     // -4px 补偿两侧各2px
  TILE_SIZE - 4
);
```

内缩 2px 是细节处理:如果不内缩,3px 的描边会与相邻格子的网格线重叠,视觉上不够清晰。内缩后,金色选中框出现在格子内部,视觉分离明显。

七、半透明高亮技术

```
// 追加两位十六进制透明度到颜色字符串
ctx.fillStyle = MOVABLE_COLOR + '80';    // '80' ≈ 128/255 ≈ 50% 透明度
ctx.fillStyle = ATTACKABLE_COLOR + '80';
```

8 位 RGBA 十六进制颜色 `#RRGGBBAA`:
- `'80'` (hex) = 128 (dec) ≈ 50% 不透明
- `'FF'` = 100% 不透明
- `'40'` = 25% 不透明

这种技巧简单直接,不需要使用 `rgba()` 函数,避免了解析颜色字符串的麻烦。

八、坐标系统

游戏使用了两套坐标系:

格子坐标(逻辑坐标)
- 范围:x ∈ [0, 8],y ∈ [0, 7]
- 来源:地图数组下标、单位 position
- 整数值

像素坐标(渲染坐标)
- 范围:x ∈ [0, 576],y ∈ [0, 512]
- 来源:格子坐标 × TILE_SIZE
- 用于 Canvas API 调用

转换公式:

```// 格子 → 像素(左上角)
const pixelX = tileX * TILE_SIZE;
const pixelY = tileY * TILE_SIZE;

// 像素 → 格子(触摸点)
const tileX = Math.floor(touchX / TILE_SIZE);
const tileY = Math.floor(touchY / TILE_SIZE);
```

九、渲染性能优化

本项目每次状态变化后调用 `drawGameMap()` 全量重绘,对于 9×8 小地图性能足够。

在大型游戏中,可以考虑**脏矩形优化**:

```

// 只重绘发生变化的格子
drawTile(x: number, y: number): void {
  ctx.clearRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
  // 重绘该格子的地形、高亮、单位
}
```

十、小结

本游戏的 Canvas 渲染系统采用**分层绘制**策略,通过地形层→高亮层→单位层→选中层的绘制顺序,实现了清晰的视觉层次。血条的三段式颜色设计、选中框的内缩处理、半透明高亮技术,都是游戏 UI 开发中值得学习的实用技巧。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐