# HarmonyOS NEXT 三国争霸策略游戏开发实战 —— 基于 ArkTS 的回合制领土征服游戏
HarmonyOS NEXT 三国争霸策略游戏开发实战 —— 基于 ArkTS 的回合制领土征服游戏
开发环境: DevEco Studio 5.0+ / HarmonyOS NEXT 6.1.1(API 24)
开发语言: ArkTS(基于 TypeScript 的鸿蒙原生声明式语言)
游戏类型: 回合制领土征服策略游戏
代码总量: 605 行(单页面全功能游戏)


一、引言
三国题材是中国游戏史上最经久不衰的题材之一。从经典策略游戏《三国志》系列到《全面战争:三国》,从卡牌游戏《三国杀》到 MOBA《王者荣耀》中的三国英雄,这段群雄逐鹿、英雄辈出的历史为游戏设计提供了取之不尽的灵感源泉。
在移动端,回合制策略游戏因其"深思熟虑而后动"的特性,拥有稳定的受众群体。相比于需要快速反应的动作游戏,策略游戏更注重局势判断、资源管理和长线规划——这些恰恰是 ArkTS 声明式 UI 和响应式状态管理所擅长的领域。
本文将通过在 HarmonyOS NEXT 上构建一个完整的三国争霸领土征服游戏,展示如何使用 ArkTS 实现:
- 多阵营选择:魏/蜀/吴三国可选,各有不同初始布局
- 3×3 领土网格:九块历史州郡组成的策略地图
- 回合制战斗:选兵→进攻→结算的完整战斗循环
- AI 对手:非玩家势力自动进行战略决策
- 中立吞并:中立领土自动被周边强势力兼并
- 统一胜利:消灭所有敌对势力即为统一天下
二、游戏设计
2.1 九州的格局
游戏地图基于三国时期的九州划分,简化为 3×3 网格:
北方 中原 南方
┌──────┐ ┌──────┐ ┌──────┐
西 │ 并州 │ │ 豫州 │ │ 益州 │
│ 魏 │ │ 中 │ │ 蜀 │
└──────┘ └──────┘ └──────┘
┌──────┐ ┌──────┐ ┌──────┐
中 │ 冀州 │ │ 兖州 │ │ 荆州 │
│ 魏 │ │ 魏 │ │ 蜀 │
└──────┘ └──────┘ └──────┘
┌──────┐ ┌──────┐ ┌──────┐
东 │ 幽州 │ │ 徐州 │ │ 扬州 │
│ 中 │ │ 吴 │ │ 吴 │
└──────┘ └──────┘ └──────┘
2.2 初始势力分配
| 势力 | 初始领土 | 总兵力 | 特点 |
|---|---|---|---|
| 魏 🦅 | 冀州(600)、并州(400)、兖州(500) | 1500 | 北方优势,兵力最厚 |
| 蜀 🐉 | 荆州(550)、益州(600) | 1150 | 西南屏障,地形险要 |
| 吴 🌊 | 徐州(450)、扬州(500) | 950 | 东南水乡,发展潜力 |
| 中 🏴 | 幽州(300)、豫州(400) | 700 | 中立地带,待价而沽 |
2.3 核心循环
┌─────────────────────────────────────────────────┐
│ 三国争霸核心循环 │
│ │
│ 选择阵营 → 查看地图 → 选择己方领土 │
│ ↓ │
│ 高亮相邻敌方领土 │
│ ↓ │
│ 点击目标 → 战斗结算 │
│ ↙ ↘ │
│ 胜利(占领) 失败(退兵) │
│ ↘ ↙ │
│ 检查胜利条件 │
│ ↙ ↘ │
│ 统一天下◄─┬──►继续战斗 │
│ │ │
│ 结束回合 │
│ ↓ │
│ AI 势力自动行动 │
│ ↓ │
│ 中立领土吞并 │
│ ↓ │
│ 进入下一回合 │
└─────────────────────────────────────────────────┘
三、数据模型
3.1 Territory 接口
interface Territory {
name: string; // 州郡名称,如 "冀州"
owner: string; // 所属势力,"魏"|"蜀"|"吴"|"中"
troops: number; // 驻兵数量
emblem: string; // 地形图标,如 "🏔️"
}
四个字段构成了每个领土的完整属性。owner 字符串同时服务于显示(魏国/蜀国/吴国/中立)和逻辑(判断是否可攻击)。
3.2 邻接关系映射
private readonly adjacentMap: number[][] = [
[1, 3], // 0 冀州 → 并州、兖州
[0, 2, 4], // 1 并州 → 冀州、幽州、豫州
[1, 5], // 2 幽州 → 并州、徐州
[0, 4, 6], // 3 兖州 → 冀州、豫州、荆州
[1, 3, 5, 7], // 4 豫州 → 并州、兖州、徐州、扬州
[2, 4, 8], // 5 徐州 → 幽州、豫州、益州
[3, 7], // 6 荆州 → 兖州、扬州
[4, 6, 8], // 7 扬州 → 豫州、荆州、益州
[5, 7], // 8 益州 → 徐州、扬州
];
邻接映射定义了九块领土之间的连接关系,是游戏的核心寻路数据。每个索引对应的领土通过邻接列表与相邻领土相连——只有相邻的敌方领土才能被攻击。
3.3 阵营颜色与图标
private readonly KINGDOM_COLORS: Record<string, string> = {
'魏': '#DC143C', // crimson red
'蜀': '#228B22', // forest green
'吴': '#1E90FF', // dodger blue
'中': '#8B8B8B', // gray
};
三国的代表色——魏红、蜀绿、吴蓝——在 UI 中用于领土边框、阵营名称等元素,让玩家一眼就能识别领土归属。
四、状态设计与 UI 架构
4.1 20 个状态变量
@Component
struct ThreeKingdoms {
// ─── 游戏数据(5个) ───
@State territories: Territory[] = []; // 9块领土数据
@State currentTurn: string = '魏'; // 当前回合势力
@State turnCount: number = 1; // 回合数
@State playerKingdom: string = '魏'; // 玩家所选势力
@State logMessages: string[] = []; // 战报日志
// ─── 选区状态(4个) ───
@State selectedIndex: number = -1; // 选中的领土索引
@State attackTargets: number[] = []; // 可攻击的目标索引列表
@State phase: string = 'select'; // 当前阶段
// ─── 战斗显示(4个) ───
@State showBattleResult: boolean = false;
@State battleLog: string = '';
@State battleAttacker: string = '';
@State battleDefender: string = '';
@State battleWin: boolean = false;
// ─── 游戏控制(4个) ───
@State gameOver: boolean = false;
@State gameResult: string = '';
@State showKingdomSelect: boolean = true;
@State aiThinking: boolean = false;
}
4.2 双阶段 UI
游戏在启动时首先显示阵营选择界面,选择后切换至游戏主界面:
build() {
Column() {
if (this.showKingdomSelect) {
this.buildKingdomSelect(); // 选择阵营
} else {
this.buildGameUI(); // 游戏主界面
}
}
.backgroundColor('#1A0A00') // 古风深棕色背景
}
这种双阶段 UI 通过一个 @State showKingdomSelect 变量控制,避免了页面跳转的复杂性。
4.3 阵营选择界面
@Builder
buildKingdomSelect() {
Column() {
Text('⚔️ 三国争霸').fontSize(28).fontWeight(FontWeight.Bold)
.fontColor('#FFD700').letterSpacing(4)
Text('选择你的阵营').fontSize(16).fontColor('#CC9966')
this.buildKingdomCard('魏', '🦅', 'DC143C', '坐拥北方,兵强马壮', () => {
this.selectKingdom('魏');
})
this.buildKingdomCard('蜀', '🐉', '228B22', '天府之国,仁德之师', () => {
this.selectKingdom('蜀');
})
this.buildKingdomCard('吴', '🌊', '1E90FF', '江东基业,水师精锐', () => {
this.selectKingdom('吴');
})
}
}
每张阵营卡片使用对应势力的颜色作为边框(border: { width: 2, color: '#' + color }),形成强烈的身份认同感。
五、地图与领土交互
5.1 地图网格渲染
@Builder
buildMapGrid() {
Column() {
ForEach(this.range(0, 3), (row: number) => {
Row() {
ForEach(this.range(0, 3), (col: number) => {
this.buildTerritoryCard(row * 3 + col)
}, (col: number) => col.toString())
}
.justifyContent(FlexAlign.Center)
.padding({ top: 3, bottom: 3 })
}, (row: number) => row.toString())
}
}
嵌套 ForEach 遍历 3×3 网格。row * 3 + col 将二维坐标映射为一维索引。
5.2 领土卡片
@Builder
buildTerritoryCard(index: number) {
Button() {
Column() {
Text(this.getTerritoryEmblem(index)).fontSize(22)
Text(this.getTerritoryName(index)).fontSize(13).fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text(this.getTerritoryOwnerLabel(index))
.fontSize(10).fontColor('#' + this.getTerritoryColor(index))
Text('兵: ' + this.getTerritoryTroops(index)).fontSize(10).fontColor('#CC9966')
}
}
.backgroundColor(index === this.selectedIndex ? '#3A3A1A'
: this.attackTargets.includes(index) ? '#3A1A1A' : '#1A0A00')
.border({
width: index === this.selectedIndex ? 2
: this.attackTargets.includes(index) ? 2 : 1,
color: index === this.selectedIndex ? '#FFD700'
: this.attackTargets.includes(index) ? '#FF4444'
: '#' + this.getTerritoryColor(index)
})
}
领土卡片的四种视觉状态:
| 状态 | 背景色 | 边框色 | 触发条件 |
|---|---|---|---|
| 普通 | #1A0A00 |
所属势力色 | 默认 |
| 已选中 | #3A3A1A |
金色 #FFD700 |
点击己方领土 |
| 可攻击 | #3A1A1A |
红色 #FF4444 |
选中己方后相邻敌方 |
| 禁用 | 不变 | 不变 | AI 回合或游戏结束 |
辅助方法(解决 @Builder 不能声明变量的限制):
getTerritoryEmblem(index: number): string {
return this.territories[index]?.emblem ?? '';
}
getTerritoryName(index: number): string {
return this.territories[index]?.name ?? '';
}
getTerritoryOwnerLabel(index: number): string {
const owner = this.territories[index]?.owner ?? '';
return owner === '中' ? '中立' : owner + '国';
}
getTerritoryColor(index: number): string {
return this.KINGDOM_COLORS[this.territories[index]?.owner ?? ''] ?? '#8B8B8B';
}
getTerritoryTroops(index: number): number {
return this.territories[index]?.troops ?? 0;
}
为什么需要这些辅助方法?
在 ArkTS 中,@Builder 方法体内不能包含 let/const 声明。如果直接在 buildTerritoryCard 中写 const t = this.territories[index],编译器会报错。将这些访问操作抽取为辅助方法,既符合 ArkTS 语法要求,又使代码更清晰。
六、战斗系统
6.1 领土点击逻辑
onTerritoryClick(index: number): void {
if (this.gameOver || this.aiThinking) return;
if (this.currentTurn !== this.playerKingdom) return;
const t = this.territories[index];
// 点击高亮目标 → 执行攻击
if (this.attackTargets.includes(index)) {
this.doAttack();
return;
}
// 点击己方领土 → 选中,显示可攻击目标
if (t.owner === this.playerKingdom) {
this.selectedIndex = index;
this.attackTargets = this.getAdjacentEnemyTerritories(index);
return;
}
// 点击其他 → 取消选择
this.selectedIndex = -1;
this.attackTargets = [];
}
交互三按钮模式:
1. 点击己方领土 → 选中(金色高亮),相邻敌方变红色高亮
2. 点击红色高亮敌方 → 执行攻击
3. 点击空地 → 取消选择
6.2 邻接敌方检测
getAdjacentEnemyTerritories(index: number): number[] {
const owner = this.territories[index].owner;
return this.adjacentMap[index].filter(i =>
this.territories[i].owner !== owner
&& this.territories[i].owner !== '中'
);
}
通过 adjacentMap 找出所有相邻领土,然后过滤掉同势力和中立的领土,只保留可攻击的敌方目标。
6.3 战斗结算
doAttack(): void {
const attacker = this.territories[this.selectedIndex];
const defender = this.territories[this.attackTargets[0]];
// 战斗力计算(含随机因子)
const atkPower = attacker.troops
+ Math.floor(Math.random() * attacker.troops * 0.4);
const defPower = defender.troops
+ Math.floor(Math.random() * defender.troops * 0.3);
const win = atkPower > defPower;
// 损失计算
const atkLoss = Math.floor(attacker.troops * (0.2 + Math.random() * 0.3));
const defLoss = Math.floor(defender.troops * (0.4 + Math.random() * 0.4));
if (win) {
attacker.troops = Math.max(1, attacker.troops - atkLoss);
defender.owner = attacker.owner; // 占领
defender.troops = Math.max(1, defender.troops - defLoss);
} else {
attacker.troops = Math.max(1, attacker.troops - atkLoss);
}
}
战斗公式:
| 角色 | 战斗力 | 损失 | 胜果 |
|---|---|---|---|
| 攻击方 | 兵力 + random(0, 兵力×40%) |
兵力 × (20% + random(0, 30%)) |
占领+保留剩余 |
| 防守方 | 兵力 + random(0, 兵力×30%) |
兵力 × (40% + random(0, 40%)) |
兵力减少 |
设计要点:
- 攻击方有更高的随机上限(40% vs 30%),鼓励主动进攻
- 防守方损失更大(40-80% vs 20-50%),体现攻城战对防守方的消耗
Math.max(1, ...)确保兵力不会归零——即使惨胜也有兵留守- 随机因子让战斗结果存在不确定性,兵力优势不等于绝对胜利
七、AI 系统
7.1 AI 回合执行
endPlayerTurn(): void {
this.selectedIndex = -1;
this.attackTargets = [];
this.showBattleResult = false;
this.runAITurns();
}
runAITurns(): void {
const kingdoms = ['魏', '蜀', '吴'].filter(k => k !== this.playerKingdom);
this.aiThinking = true;
for (const kingdom of kingdoms) {
const aiTerritories = this.territories.filter(
t => t.owner === kingdom && t.troops > 0
);
if (aiTerritories.length === 0) continue;
// 每个 AI 势力每回合做 1-2 次进攻
const actions = 1 + Math.floor(Math.random() * 2);
for (let a = 0; a < actions; a++) {
// 随机选一块己方领土作为进攻出发点
const idx = this.territories.indexOf(
aiTerritories[Math.floor(Math.random() * aiTerritories.length)]
);
// 找出可攻击的敌方领土
const targets = this.getAdjacentEnemyTerritories(idx);
const realTargets = targets.filter(
i => this.territories[i].owner !== '中'
&& this.territories[i].owner !== kingdom
);
if (realTargets.length > 0 && this.territories[idx].troops > 100) {
// 随机选一个目标进攻
const targetIdx = realTargets[Math.floor(Math.random() * realTargets.length)];
this.executeAIAttack(idx, targetIdx, kingdom);
}
}
}
this.aiThinking = false;
this.turnCount++;
this.currentTurn = this.playerKingdom;
this.absorbNeutral();
}
AI 行为逻辑:
| 决策 | 策略 | 说明 |
|---|---|---|
| 进攻次数 | 每回合 1-2 次 | 随机决定,避免 AI 过于集中 |
| 进攻出发地 | 随机选取 | 从所有己方领土中随机挑选 |
| 进攻目标 | 随机选取 | 从相邻敌方领土中随机挑选 |
| 兵力门槛 | > 100 兵才进攻 | 避免 AI 做无谓的牺牲 |
7.2 中立吞并
absorbNeutral(): void {
const neutrals = this.territories.filter(t => t.owner === '中');
for (const n of neutrals) {
const idx = this.territories.indexOf(n);
const neighbors = this.adjacentMap[idx];
for (const ni of neighbors) {
const neighbor = this.territories[ni];
if (neighbor.owner !== '中' && n.troops < neighbor.troops) {
n.owner = neighbor.owner;
this.addLog('🏴 ' + n.name + '被' + neighbor.owner + '国吞并');
break;
}
}
}
}
中立领土在每回合结束时,会被相邻且兵力更强的势力自动吞并。这个机制模拟了"弱肉强食"的乱世法则,也确保中立领土不会长期闲置。
八、胜利条件判断
checkWinCondition(): void {
const kingdoms = ['魏', '蜀', '吴'];
const alive = kingdoms.filter(k =>
this.territories.some(t => t.owner === k)
);
// 只剩一个势力 → 统一天下
if (alive.length === 1) {
this.gameOver = true;
if (alive[0] === this.playerKingdom) {
this.gameResult = '🎉 恭喜统一天下!\n' + this.playerKingdom + '国一统中原!';
} else {
this.gameResult = '😢 ' + alive[0] + '国统一了天下...';
}
}
// 玩家被灭
if (!this.territories.some(t => t.owner === this.playerKingdom)) {
this.gameOver = true;
this.gameResult = '💀 你的' + this.playerKingdom + '国已被消灭...';
}
}
两种失败条件:
- 其他势力统一了天下(玩家的势力还活着但不再独立)
- 玩家的势力彻底被消灭(所有领土被夺走)
九、主题与视觉设计
9.1 古风配色
应用采用古风深色主题,营造三国时代的厚重感:
| 色值 | 用途 | 说明 |
|---|---|---|
#1A0A00 |
主背景 | 古风深棕 |
#2A1A0A |
卡片/标题栏 | 浅棕 |
#3A1A0A |
可攻击高亮 | 暗红 |
#3A3A1A |
已选中高亮 | 暗金 |
#FFD700 |
标题/选中/己方 | 金色 |
#CC9966 |
次要文字 | 古铜色 |
#DC143C |
魏国色 | 红 |
#228B22 |
蜀国色 | 绿 |
#1E90FF |
吴国色 | 蓝 |
#8B8B8B |
中立/禁用 | 灰 |
9.2 游戏结束弹窗
@Builder
buildGameOverOverlay() {
Column() {
Column() {
Text('🏆').fontSize(64)
Text(this.gameResult).fontSize(22).fontWeight(FontWeight.Bold)
.fontColor('#FFD700').textAlign(TextAlign.Center)
Button('🔄 重新开局')
.fontColor('#FFFFFF').backgroundColor('#CC3333')
.borderRadius(16).height(44).width(160)
.onClick(() => { this.initGame(); })
}
.padding(24).backgroundColor('#2A1A0A')
.borderRadius(20).border({ width: 2, color: '#FFD700' })
}
.backgroundColor('#AA000000')
}
胜利/失败共用同一个弹窗,通过 gameResult 的文字内容区分结局。
十、构建验证
hvigorw assembleHap --daemon=false --analyze=false
构建成功输出:
Finished :entry:default@CompileArkTS... after 8 s 417 ms
Finished :entry:default@PackageHap... after 604 ms
Finished :entry:default@PackingCheck... after 7 ms
BUILD SUCCESSFUL in 12 s 300 ms
编译错误修复
在开发过程中修复了 11 个编译错误,全部为同一类问题:
错误:@Builder 中的变量声明
ERROR: Only UI component syntax can be written here.
File: index9.ets:200, 201, 202, 222, 235-241 (11 errors)
原因:在三个 @Builder 方法中声明了局部变量:
buildKingdomStatus:const territories/filter/reducebuildMapGrid:const idx = row * 3 + colbuildTerritoryCard:const t/color/isSelected/isAttackable/isPlayerOwned
修复策略:将所有变量访问内联或抽取为普通成员方法:
// ❌ 错误
@Builder buildTerritoryCard(index: number) {
const t = this.territories[index];
const isSelected = index === this.selectedIndex;
// ...
}
// ✅ 正确:抽取为辅助方法
getTerritoryName(index: number): string {
return this.territories[index]?.name ?? '';
}
getTerritoryColor(index: number): string {
return this.KINGDOM_COLORS[this.territories[index]?.owner ?? ''] ?? '#8B8B8B';
}
十一、扩展方向
11.1 武将系统
每个领土可以驻扎一名武将,武将具有统率、武力、智力属性,影响战斗结果:
interface General {
name: string;
leadership: number; // 统率:影响兵力上限
武力: number; // 武力:影响攻击力
智力: number; // 智力:影响防守力
}
11.2 内政系统
增加内政操作——征兵、屯田、筑城。玩家可以在自己的领土上进行内政操作,提升兵力和防御。
11.3 外交系统
增加结盟、停战、求援等外交选项。两个弱国可以联合对抗强国,增加游戏的策略深度。
11.4 更多地图
可以增加更多领土(如 4×4 或 5×5 网格),加入交州、凉州、雍州等更多州郡,使地图更接近真实的三国版图。
11.5 保存/读档
使用 @kit.ArkData 的 Preferences API 保存游戏进度:
async function saveGame(context: Context, territories: Territory[]) {
const pref = await preferences.getPreferences(context, 'threeKingdoms');
await pref.put('saveData', JSON.stringify(territories));
await pref.flush();
}
十二、总结
本文通过一个完整的三国争霸策略游戏,展示了在 HarmonyOS NEXT 上使用 ArkTS 开发回合制策略游戏的全流程。从九州的领土布局、三国的势力分配,到选兵进攻的战斗循环、AI 的自动决策,游戏虽小(605 行),但包含了回合制策略游戏的核心机制。
核心技术点回顾
| 技术点 | 应用位置 | 关键实现 |
|---|---|---|
| 邻接映射 | adjacentMap |
索引数组定义领土连接关系 |
| 双阶段 UI | build() |
showKingdomSelect 控制显示 |
| 领土状态着色 | buildTerritoryCard |
三态条件判断:selected/attackable/normal |
| 战斗公式 | doAttack() |
兵力 + random(0~40%) 对比判定 |
| AI 决策 | runAITurns() |
随机选兵+随机选目标+兵力门槛 |
| 中立吞并 | absorbNeutral() |
对比邻接势力兵力,强者吞并弱者 |
| @Builder 限制 | 三处修复 | 所有变量抽取为普通成员方法 |
| 胜利判定 | checkWinCondition() |
存活势力计数 + 玩家存活检测 |
ArkTS 开发策略游戏的优势
- @State 驱动游戏状态:领土数据、战斗结果、回合信息的变化自动同步 UI
- 条件渲染简化 UI 逻辑:
if (showKingdomSelect) / else优雅处理双阶段 - ForEach 网格渲染:嵌套 ForEach 遍历 3×3 地图,代码简洁
- 类型安全减少 bug:TypeScript 的类型系统确保了数据格式的正确性
三国争霸虽然是简化版的策略游戏,但它涵盖了回合制策略游戏的核心机制——领地控制、战斗计算、AI 决策、胜利条件。这些知识可以轻松扩展到更复杂的策略游戏开发中。希望本文能为你在 HarmonyOS NEXT 上开发策略游戏提供有价值的参考。
服游戏
更多推荐



所有评论(0)