第7篇 | 基于 HarmonyOS 开发战旗小游戏项目游戏主页面
前言:
游戏主界面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; // 格子中心 Yctx.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 开发中值得学习的实用技巧。
更多推荐


所有评论(0)