HarmonyOS NEXT 三国争霸策略游戏开发实战 —— 基于 ArkTS 的回合制领土征服游戏

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


在这里插入图片描述
在这里插入图片描述

一、引言

三国题材是中国游戏史上最经久不衰的题材之一。从经典策略游戏《三国志》系列到《全面战争:三国》,从卡牌游戏《三国杀》到 MOBA《王者荣耀》中的三国英雄,这段群雄逐鹿、英雄辈出的历史为游戏设计提供了取之不尽的灵感源泉。

在移动端,回合制策略游戏因其"深思熟虑而后动"的特性,拥有稳定的受众群体。相比于需要快速反应的动作游戏,策略游戏更注重局势判断、资源管理和长线规划——这些恰恰是 ArkTS 声明式 UI 和响应式状态管理所擅长的领域。

本文将通过在 HarmonyOS NEXT 上构建一个完整的三国争霸领土征服游戏,展示如何使用 ArkTS 实现:

  1. 多阵营选择:魏/蜀/吴三国可选,各有不同初始布局
  2. 3×3 领土网格:九块历史州郡组成的策略地图
  3. 回合制战斗:选兵→进攻→结算的完整战斗循环
  4. AI 对手:非玩家势力自动进行战略决策
  5. 中立吞并:中立领土自动被周边强势力兼并
  6. 统一胜利:消灭所有敌对势力即为统一天下

二、游戏设计

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 + '国已被消灭...';
  }
}

两种失败条件:

  1. 其他势力统一了天下(玩家的势力还活着但不再独立)
  2. 玩家的势力彻底被消灭(所有领土被夺走)

九、主题与视觉设计

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/reduce
  • buildMapGrid: const idx = row * 3 + col
  • buildTerritoryCard: 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 开发策略游戏的优势

  1. @State 驱动游戏状态:领土数据、战斗结果、回合信息的变化自动同步 UI
  2. 条件渲染简化 UI 逻辑if (showKingdomSelect) / else 优雅处理双阶段
  3. ForEach 网格渲染:嵌套 ForEach 遍历 3×3 地图,代码简洁
  4. 类型安全减少 bug:TypeScript 的类型系统确保了数据格式的正确性

三国争霸虽然是简化版的策略游戏,但它涵盖了回合制策略游戏的核心机制——领地控制、战斗计算、AI 决策、胜利条件。这些知识可以轻松扩展到更复杂的策略游戏开发中。希望本文能为你在 HarmonyOS NEXT 上开发策略游戏提供有价值的参考。
服游戏

Logo

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

更多推荐