# HarmonyOS NEXT Roguelike 地牢探险游戏开发实战 —— 基于 ArkTS 的回合制肉鸽游戏

HarmonyOS NEXT Roguelike 地牢探险游戏开发实战 —— 基于 ArkTS 的回合制肉鸽游戏

开发环境: DevEco Studio 5.0+ / HarmonyOS NEXT 6.1.1(API 24)
开发语言: ArkTS(基于 TypeScript 的鸿蒙原生声明式语言)
游戏类型: Roguelike(肉鸽)回合制地牢探险
代码总量: 705 行(单页面全功能游戏)


一、引言

Roguelike(国内常称"肉鸽")是电子游戏史上最具影响力的类型之一。自 1980 年《Rogue》诞生以来,其核心设计理念——程序化生成(Procedural Generation)、永久死亡(Permadeath)和资源管理(Resource Management)——已经影响了无数经典作品,从《NetHack》到《以撒的结合》,从《Faster Than Light》到《Hades》。

在移动端,Roguelike 游戏因其"一把一局"的短平快特性、随机的重复可玩性和逐步成长的成就感,一直受到玩家的喜爱。然而,在传统的移动开发框架中实现一个 Roguelike 游戏需要处理复杂的网格渲染、碰撞检测、AI 行为和状态管理。

本文将通过在 HarmonyOS NEXT 上使用 ArkTS 构建一个完整的地牢探险 Roguelike 游戏,展示如何利用声明式 UI 和响应式状态管理,以不到 800 行代码实现一个可玩的 Roguelike 游戏。

游戏核心特性

特性 说明
程序化地图 房间+走廊算法,每层随机生成不同的 9×9 地牢
回合制战斗 走向敌人触发战斗,玩家先攻→敌人反击
8 种敌人 史莱姆→骷髅→哥布林→蝙蝠→石像鬼→刺客→火焰魔→龙
成长系统 击杀得 XP → 升级(HP/ATK/DEF 全提升)
道具系统 ❤️回血 ⚔️武器 🛡️盔甲 三种拾取物
永久死亡 HP归零 → 游戏结束 → 重置重来
胜利条件 到达第 10 层 → 通关 ✨
难度递增 每层敌人属性 ×1.3 缩放,更强敌人类型解锁

二、Roguelike 核心系统设计

2.1 游戏循环

地牢探险的游戏循环遵循经典的 Roguelike 模式:

┌─────────────────────────────────────────────────┐
│              地牢探险核心循环                       │
│                                                   │
│  生成楼层 → 探索地图 → 遭遇敌人 → 回合战斗        │
│               ↓                     ↓            │
│           拾取道具             胜利或死亡          │
│               ↓                                   │
│           找到楼梯 → 下一层(难度增加)             │
│               ↓                                   │
│           第10层通关 → 胜利 ✨                    │
└─────────────────────────────────────────────────┘

2.2 数据模型

游戏定义了三个接口来管理所有游戏实体:

// 房间数据(用于地图生成)
interface Room {
  x: number;
  y: number;
  w: number;
  h: number;
}

// 敌人实例(动态生成,含当前属性)
interface EnemyData {
  name: string;       // 名称
  symbol: string;     // 地图符号(S/K/G/B/R/A/F/D)
  color: string;      // 显示颜色
  hp: number;         // 当前血量
  maxHp: number;      // 最大血量
  atk: number;        // 攻击力
  def: number;        // 防御力
  xpReward: number;   // 击杀经验
}

// 敌人模板(基础属性,用于在不同楼层缩放)
interface EnemyTemplate {
  name: string;
  symbol: string;
  color: string;
  hpBase: number;
  atkBase: number;
  defBase: number;
  xpReward: number;
}

// 道具数据
interface ItemData {
  type: string;    // 'health' | 'weapon' | 'armor'
  symbol: string;  // 显示符号
  value: number;   // 恢复/加成数值
}

设计原则: 模板(EnemyTemplate)和实例(EnemyData)分离。模板存储基础属性,实际游戏中根据楼层进行缩放生成实例。

2.3 状态变量全景

游戏使用了 18 个 @State 变量分为四个类别:

// ─── 玩家属性(7个) ───
@State playerHP: number = 30;
@State playerMaxHP: number = 30;
@State playerATK: number = 5;
@State playerDEF: number = 2;
@State playerLevel: number = 1;
@State playerXP: number = 0;
@State xpToNext: number = 10;

// ─── 游戏进度(4个) ───
@State floor: number = 1;
@State enemiesKilled: number = 0;
@State gameOver: boolean = false;
@State gameWin: boolean = false;

// ─── 地图数据(5个) ───
@State grid: string[][] = [];               // 地形网格
@State enemyGrid: (EnemyData | null)[][] = []; // 敌人位置
@State itemGrid: (ItemData | null)[][] = [];   // 道具位置
@State playerX: number = 4;
@State playerY: number = 4;

// ─── UI 状态(2个) ───
@State logMessages: string[] = [...];

关键设计决策: 地图数据使用二维数组而非对象列表。grid[y][x] 通过坐标 O(1) 访问,比遍历对象列表高效得多。三个独立的网格层(地形、敌人、道具)通过坐标对齐,组合成完整的游戏世界状态。


三、程序化地图生成

3.1 初始化

generateFloor(): void {
  // 创建三个独立的二维网格
  const newGrid: string[][] = [];
  const newEnemyGrid: (EnemyData | null)[][] = [];
  const newItemGrid: (ItemData | null)[][] = [];

  for (let y = 0; y < this.MAP_SIZE; y++) {
    newGrid[y] = [];
    newEnemyGrid[y] = [];
    newItemGrid[y] = [];
    for (let x = 0; x < this.MAP_SIZE; x++) {
      newGrid[y][x] = this.TILE_WALL;   // 默认全是墙壁
      newEnemyGrid[y][x] = null;
      newItemGrid[y][x] = null;
    }
  }

  // 1. 挖房间和走廊
  this.carveRooms(newGrid);
  // 2. 放置玩家
  // 3. 放置敌人
  // 4. 放置道具
  // 5. 放置楼梯
  // 6. 赋值到 @State
  this.grid = newGrid;
  this.enemyGrid = newEnemyGrid;
  this.itemGrid = newItemGrid;
}

3.2 房间+走廊算法

这是 Roguelike 地图生成的核心算法:

carveRooms(grid: string[][]): void {
  const roomCount = 3 + Math.floor(Math.random() * 3);  // 3-5个房间
  const rooms: Room[] = [];

  for (let i = 0; i < roomCount; i++) {
    // 每个房间2-4格宽/高
    const rw = 2 + Math.floor(Math.random() * 3);
    const rh = 2 + Math.floor(Math.random() * 3);
    const rx = 1 + Math.floor(Math.random() * (this.MAP_SIZE - rw - 1));
    const ry = 1 + Math.floor(Math.random() * (this.MAP_SIZE - rh - 1));

    // 挖出矩形房间
    for (let y = ry; y < ry + rh; y++) {
      for (let x = rx; x < rx + rw; x++) {
        if (y >= 0 && y < this.MAP_SIZE && x >= 0 && x < this.MAP_SIZE) {
          grid[y][x] = this.TILE_FLOOR;  // 将墙壁改为地板
        }
      }
    }
    rooms.push({ x: rx, y: ry, w: rw, h: rh });
  }

  // 连接相邻房间(走廊):先水平再垂直
  for (let i = 1; i < rooms.length; i++) {
    const prev = rooms[i - 1];
    const curr = rooms[i];
    const cx = Math.floor(prev.x + prev.w / 2);  // 上一个房间中心
    const cy = Math.floor(prev.y + prev.h / 2);
    const nx = Math.floor(curr.x + curr.w / 2);  // 当前房间中心
    const ny = Math.floor(curr.y + curr.h / 2);

    // 水平走廊
    for (let x = Math.min(cx, nx); x <= Math.max(cx, nx); x++) {
      grid[cy][x] = this.TILE_FLOOR;
    }
    // 垂直走廊
    for (let y = Math.min(cy, ny); y <= Math.max(cy, ny); y++) {
      grid[y][nx] = this.TILE_FLOOR;
    }
  }
}

算法步骤:

  1. 决定 3-5 个房间的数量和位置(确保不越界)
  2. 在每个房间位置,将墙体替换为地板
  3. 将房间中心两两相连,形成 L 形走廊
  4. 未挖通的区域保持为墙壁

9×9 地图示例输出(符号示意):

█████████
█·······█
█·███··██
█·······█
████·██·█
█·····@·█
█··G····█
█··▼····█
█████████

· = 地板, = 墙壁,@ = 玩家,G = 哥布林, = 楼梯

3.3 实体放置逻辑

放置敌人:

// 3-5个敌人,曼哈顿距离≥3远离玩家
const enemyCount = 3 + Math.floor(Math.random() * 3);
// 随机尝试放置,最多尝试100次
while (placed < enemyCount && attempts < 100) {
  const ex = Math.floor(Math.random() * this.MAP_SIZE);
  const ey = Math.floor(Math.random() * this.MAP_SIZE);
  const dist = Math.abs(ex - px) + Math.abs(ey - py);
  if (grid[ey][ex] === this.TILE_FLOOR && enemyGrid[ey][ex] === null
    && dist >= 3 && !(ex === px && ey === py)) {
    const template = this.getRandomEnemy();
    // 楼层缩放:每层属性×1.3
    const scale = 1 + (this.floor - 1) * 0.3;
    enemyGrid[ey][ex] = {
      hp: Math.floor(template.hpBase * scale),
      atk: Math.floor(template.atkBase * scale),
      // ...
    };
    placed++;
  }
}

敌人类型解锁: 随着楼层增加,更强的敌人类型出现:

getRandomEnemy(): EnemyTemplate {
  // 第1-2层:只能出史莱姆和骷髅(索引0-2)
  // 第3-4层:出现蝙蝠和哥布林(索引0-3)
  // 第5-6层:出现石像鬼和刺客(索引0-5)
  // ...
  const maxIndex = Math.min(this.enemyTypes.length, 2 + Math.floor(this.floor / 2));
  return this.enemyTypes[Math.floor(Math.random() * maxIndex)];
}
楼层 可用敌人 缩放系数
第 1 层 史莱姆、骷髅 1.0×
第 2 层 + 哥布林、蝙蝠 1.3×
第 3 层 + 石像鬼 1.6×
第 4 层 + 暗影刺客 1.9×
第 5 层 + 火焰魔 2.2×
第 6+ 层 + 龙(全部解锁) 2.5×+

放置道具:

// 1-3个道具,加权随机
const types: string[] = ['health', 'health', 'weapon', 'armor'];
// 'health' 权重更高,更常出现
const type = types[Math.floor(Math.random() * types.length)];

每种道具的效果:

类型 效果 数值范围
health ❤️ 恢复 HP 10-19 点
weapon ⚔️ 永久提升攻击力 +1 至 +3
armor 🛡️ 永久提升防御力 +1 至 +2

放置楼梯:

楼梯位于曼哈顿距离 ≥ 4 的空地上,确保玩家不会一开始就踩在楼梯上。


四、回合制移动与战斗

4.1 玩家移动

movePlayer(dx: number, dy: number): void {
  if (this.gameOver || this.gameWin) return;

  const nx = this.playerX + dx;
  const ny = this.playerY + dy;

  // 边界检查
  if (nx < 0 || nx >= this.MAP_SIZE || ny < 0 || ny >= this.MAP_SIZE) return;
  // 墙壁检查
  if (this.grid[ny][nx] === this.TILE_WALL) return;

  // 敌人?→ 战斗
  const enemy = this.enemyGrid[ny][nx];
  if (enemy !== null) { this.battle(enemy); return; }

  // 道具?→ 拾取
  const item = this.itemGrid[ny][nx];
  if (item !== null) { this.pickupItem(item, nx, ny); }

  // 楼梯?→ 下楼
  if (this.grid[ny][nx] === this.TILE_STAIRS) { this.descendStairs(); return; }

  // 正常移动
  this.playerX = nx;
  this.playerY = ny;
}

每一次移动按照严格的优先级链判断:边界→墙体→敌人→道具→楼梯→空地移动。这种链式判断确保了游戏逻辑的确定性。

4.2 战斗系统

战斗是回合制的,玩家先手攻击,如果敌人未死则反击:

battle(enemy: EnemyData): void {
  // 玩家攻击:伤害 = max(1, ATK - 敌人DEF + 随机0~2)
  const playerDamage = Math.max(1,
    this.playerATK - enemy.def + Math.floor(Math.random() * 3));
  enemy.hp -= playerDamage;

  if (enemy.hp <= 0) {
    // 敌人死亡
    this.enemiesKilled++;
    this.playerXP += enemy.xpReward;
    this.addLog('⚔️ 你对' + enemy.name + '造成 ' + playerDamage + ' 点伤害,💀 消灭了它!');
    // 从地图移除敌人
    for (let y = 0; y < this.MAP_SIZE; y++) {
      for (let x = 0; x < this.MAP_SIZE; x++) {
        if (this.enemyGrid[y][x] === enemy) {
          this.enemyGrid[y][x] = null;
          break;
        }
      }
    }
    this.checkLevelUp();
    return;
  }

  // 敌人反击:伤害 = max(1, 敌人ATK - 玩家DEF + 随机0~1)
  const enemyDamage = Math.max(1,
    enemy.atk - this.playerDEF + Math.floor(Math.random() * 2));
  this.playerHP -= enemyDamage;
  this.addLog(',' + enemy.name + '反击造成 ' + enemyDamage + ' 点伤害');

  if (this.playerHP <= 0) {
    this.playerHP = 0;
    this.gameOver = true;
    this.addLog('💀 你被' + enemy.name + '击败了...游戏结束');
  }
}

伤害计算公式:

角色 公式 最小值
玩家攻击 ATK - enemy.DEF + random(0,2) 1
敌人反击 enemy.ATK - 玩家DEF + random(0,1) 1

随机伤害浮动(+0~2 和 +0~1)让战斗结果有一定的不确定性,但大部分情况下强者胜。

战斗流程图:

玩家走向敌人
    ↓
玩家造成 damage = max(1, ATK - def + random(0,2))
    ↓
enemy.hp -= damage
    ↓
┌─── enemy.hp ≤ 0 ? ───┐
│         是            │         否
│ 敌人死亡              │ 敌人反击
│ +XP +击杀数           │ damage = max(1, atk - DEF + random(0,1))
│ 移除敌人              │ playerHP -= damage
│ 检查升级              │      ↓
└───────────────────────┘ ┌── playerHP ≤ 0 ? ──┐
                          │     是            │     否
                          │   GAME OVER       │  战斗结束,玩家退回
                          └───────────────────┘

4.3 升级系统

checkLevelUp(): void {
  if (this.playerXP >= this.xpToNext) {
    this.playerLevel++;
    this.playerXP -= this.xpToNext;          // 经验不归零,溢出保留
    this.xpToNext = Math.floor(this.xpToNext * 1.5);  // 所需经验递增
    this.playerMaxHP += 5;                    // 永久提升血量上限
    this.playerHP = Math.min(this.playerHP + 10, this.playerMaxHP);  // 升级回血
    this.playerATK += 1;                     // 永久提升攻击
    this.playerDEF += 1;                     // 永久提升防御
    this.addLog('⬆️ 升级!达到 Lv.' + this.playerLevel
      + '(HP+10 ATK+1 DEF+1)');
  }
}

升级曲线:

等级 所需 XP 累计 XP HP 上限 ATK DEF
1 10 0 30 5 2
2 15 10 35 6 3
3 22 25 40 7 4
4 33 47 45 8 5
5 49 80 50 9 6

4.4 下楼与胜利条件

descendStairs(): void {
  this.floor++;
  this.playerHP = Math.min(this.playerMaxHP, this.playerHP + 10);  // 下楼回血
  this.addLog('⬇️ 进入第 ' + this.floor + ' 层(HP 恢复 10 点)');
  this.generateFloor();

  if (this.floor >= 10) {
    this.gameWin = true;
    this.addLog('🎉 恭喜通关!你征服了地牢!');
  }
}

每下楼一次,玩家恢复 10 点 HP,作为推进度的奖励。第 10 层通关判定在 generateFloor() 之前——实际上玩家在到达第 10 层地图时就已经胜利。


五、UI 界面构建

5.1 整体布局

build() {
  Column() {                         // 主容器(全屏)
    this.buildHeader();              // 标题栏:楼层、属性、XP进度条
    this.buildMapArea();             // 地图区域:9×9 网格渲染
    this.buildControls();            // 控制区:方向键 + 图例
    this.buildLog();                 // 日志区:战斗记录
    if (this.gameOver) this.buildGameOverOverlay();
    if (this.gameWin) this.buildGameWinOverlay();
  }
  .backgroundColor('#0A0A0A')       // 极黑背景,仿经典 Roguelike 终端
}

采用经典的竖屏布局,从上到下依次排列标题栏、地图、操作按钮和日志。

5.2 标题栏

@Builder
buildHeader() {
  Column() {
    Text('🏰 地牢探险 - 第 ' + this.floor + ' 层')
      .fontSize(18).fontWeight(FontWeight.Bold)
      .fontColor('#FFD700').letterSpacing(2)

    // 四维属性条
    Row() {
      this.buildStat('❤️', this.playerHP + '/' + this.playerMaxHP, '#FF4444')
      this.buildStat('⚔️', this.playerATK.toString(), '#FF8C00')
      this.buildStat('🛡️', this.playerDEF.toString(), '#4169E1')
      this.buildStat('⬆️', 'Lv.' + this.playerLevel, '#32CD32')
    }

    // XP 进度信息
    Row() {
      Text('XP: ' + this.playerXP + '/' + this.xpToNext).fontColor('#8B8B8B')
      Text('击杀: ' + this.enemiesKilled).fontColor('#8B8B8B')
    }

    // XP 进度条(绿色渐变)
    Row() {
      Row().width(this.getXpPercent() + '%')
        .height(4).backgroundColor('#32CD32').borderRadius(2)
    }
    .width('90%').height(4).backgroundColor('#333333').borderRadius(2)
  }
  .backgroundColor('#1A1A2E')
  .border({ width: { bottom: 2 }, color: '#FFD700' })
}

XP 进度条实现: 双层 Row 嵌套,外层(灰色)作为背景轨道,内层(绿色)的宽度百分比动态反映 XP 进度:

getXpPercent(): string {
  const ratio = Math.min(1, this.playerXP / this.xpToNext);
  return (ratio * 100).toString();
}

5.3 地图网格渲染

地图使用嵌套的 ForEach 渲染二维网格:

@Builder
buildMapArea() {
  Column() {
    // 外层:遍历行(y)
    ForEach(this.range(0, this.MAP_SIZE), (y: number) => {
      Row() {
        // 内层:遍历列(x)
        ForEach(this.range(0, this.MAP_SIZE), (x: number) => {
          Text(this.getTileSymbol(x, y))    // 获取符号
            .fontSize(18).fontWeight(FontWeight.Bold)
            .fontColor(this.getTileColor(x, y))  // 获取颜色
            .width(32).height(32)
            .textAlign(TextAlign.Center)
        }, (x: number) => x.toString())
      }
      .justifyContent(FlexAlign.Center)
    }, (y: number) => y.toString())
  }
  .backgroundColor('#111122')
}

每个格子通过两个函数决定显示内容:

// 层级优先级:玩家 > 敌人 > 道具 > 地形
getTileSymbol(x: number, y: number): string {
  if (x === this.playerX && y === this.playerY) return '@';
  const enemy = this.enemyGrid[y]?.[x];
  if (enemy !== null && enemy !== undefined) return enemy.symbol;
  const item = this.itemGrid[y]?.[x];
  if (item !== null && item !== undefined) return item.symbol;
  return this.grid[y]?.[x] ?? '█';
}

// 每个元素独立配色
getTileColor(x: number, y: number): string {
  if (x === this.playerX && y === this.playerY) return '#00FFFF';  // 青色
  const enemy = this.enemyGrid[y]?.[x];
  if (enemy !== null && enemy !== undefined) return enemy.color;    // 敌人自身颜色
  const item = this.itemGrid[y]?.[x];
  if (item !== null && item !== undefined) {
    switch (item.type) {
      case 'health': return '#FF69B4';  // 粉色
      case 'weapon': return '#FF8C00';  // 橙色
      case 'armor': return '#4169E1';   // 蓝色
    }
  }
  const tile = this.grid[y]?.[x];
  switch (tile) {
    case '·': return '#555555';         // 地板:灰色
    case '█': return '#8B4513';         // 墙壁:棕色
    case '▼': return '#FFD700';         // 楼梯:金色
    default: return '#8B4513';
  }
}

符号系统的视觉优先级:

优先级 元素 符号 颜色
1(最高) 玩家 @ 青色 #00FFFF
2 敌人 S/K/G/B/R/A/F/D 按类型不同
3 道具 ♥/⚔/🛡 粉/橙/蓝
4 楼梯 金色
5 地板 · 灰色
6(最低) 墙壁 棕色

5.4 方向键控制

@Builder
buildControls() {
  Column() {
    // 图例条
    Row() {
      Text('图例: ').fontColor('#8B8B8B')
      Text('@ 你').fontColor('#00FFFF')
      Text('S/K/G 敌人').fontColor('#FF4444')
      Text('▼ 楼梯').fontColor('#FFD700')
      Text('♥ 道具').fontColor('#FF69B4')
    }

    // 方向键(十字布局)
    Row() { Blank(); this.buildDirButton('↑', 0, -1); Blank() }
    Row() {
      this.buildDirButton('←', -1, 0)
      this.buildDirButton('↓', 0, 1)
      this.buildDirButton('→', 1, 0)
    }
  }
  .backgroundColor('#1A1A2E')
}

@Builder
buildDirButton(label: string, dx: number, dy: number) {
  Button(label)
    .fontSize(24).fontWeight(FontWeight.Bold)
    .fontColor('#FFFFFF').backgroundColor('#333355')
    .borderRadius(8).width(56).height(56).margin(4)
    .enabled(!this.gameOver && !this.gameWin)
    .onClick(() => { this.movePlayer(dx, dy); })
}

十字键布局: 上键单独一行居中,左右下键在下一行排列,模拟方向键的物理布局。游戏结束或胜利时,所有方向键通过 .enabled(false) 自动禁用。

5.5 战斗日志

@Builder
buildLog() {
  Column() {
    Text('📋 战斗日志').fontColor('#8B8B8B')
    Scroll() {
      Column() {
        ForEach(this.logMessages, (msg: string) => {
          Text(msg).fontSize(11).fontColor('#A0A0A0')
        }, (msg: string) => msg)
      }
    }
    .height(60)
  }
  .backgroundColor('#111111')
  .layoutWeight(1)  // 填充底部剩余空间
}

layoutWeight(1) 让日志区域占据所有未被其他组件占用的空间,确保在不同屏幕尺寸下都能填充底部。

5.6 游戏结束与胜利弹窗

游戏结束(死亡弹窗):

@Builder
buildGameOverOverlay() {
  Column() {
    Column() {
      Text('💀').fontSize(64)
      Text('游戏结束').fontSize(28).fontColor('#FF4444')
      Text('你倒在了第 ' + this.floor + ' 层')
      Text('击杀: ' + this.enemiesKilled + ' | 等级: Lv.' + this.playerLevel)

      Button('🔄 重新开始')
        .fontColor('#FFFFFF').backgroundColor('#FF4444')
        .borderRadius(16).height(44).width(160)
        .onClick(() => { this.resetGame(); })
    }
    .padding(24).backgroundColor('#1A1A2E')
    .borderRadius(20).border({ width: 2, color: '#FF4444' })  // 红色边框
  }
  .backgroundColor('#AA000000')  // 半透明遮罩
}

胜利(通关弹窗):

@Builder
buildGameWinOverlay() {
  // ... 类似结构,但颜色为金色 '#FFD700'
  Text('🎉').fontSize(64)
  Text('恭喜通关!').fontColor('#FFD700')
  Button('🔄 再来一次').backgroundColor('#FFD700').fontColor('#000000')
  // 金色边框
  .border({ width: 2, color: '#FFD700' })
}

两种弹窗的结构相同但配色不同:死亡用红色警示,胜利用金色庆祝。


六、游戏平衡性分析

6.1 难度曲线

楼层 敌人数 敌人缩放 道具数 推荐最低属性
1 3-4 1.0× 1-2 HP30 ATK5 DEF2
2 3-4 1.3× 1-2 HP35 ATK6 DEF3
3 3-4 1.6× 1-3 HP40 ATK7 DEF4
4 3-4 1.9× 2-3 HP45 ATK8 DEF5
5 3-5 2.2× 2-3 HP50 ATK9 DEF6
6-9 4-5 2.5×+ 2-3 HP50+ ATK10+ DEF7+

6.2 策略建议

初期(第 1-2 层):

  • 优先收集武器 ⚔️ 和盔甲 🛡️,提升基础属性
  • 避免与多个敌人近战纠缠
  • 利用回血道具 ♥ 保持满血

中期(第 3-6 层):

  • 尽量击杀每个敌人获取 XP,争取升级
  • 关注 XP 进度条,快升级时可以冒险一些
  • 龙(第 6 层开始出现)攻击极高,确保防御足够

后期(第 7-10 层):

  • 小心走位,避免被围攻
  • 每一层都搜索所有道具
  • 保持 HP 在 50% 以上再下楼

七、主题与视觉设计

7.1 暗色主题配色

游戏采用经典的终端暗色主题,致敬 Roguelike 的起源:

色值 用途 说明
#0A0A0A 页面背景 极黑,仿 CRT 屏幕
#111122 地图背景 深蓝黑,略带科技感
#1A1A2E 标题栏/控制区 深藏青
#333333 XP 进度条轨道 深灰
#00FFFF 玩家 @ 青色,高亮突出
#FFD700 标题/楼梯/胜利 金色,重要信息
#FF4444 HP/死亡弹窗 红色,危险警示
#32CD32 等级/XP 绿色,成长积极
#FF8C00 攻击力/武器 橙色
#4169E1 防御力/盔甲 蓝色
#FF69B4 回血道具 粉色

7.2 地图符号系统

符号 含义 颜色 说明
@ 玩家 青色 终端的经典玩家符号
S 史莱姆 绿色 Slime
K 骷髅兵 银灰 SKeleton
G 哥布林 橙色 Goblin
B 蝙蝠 紫色 Bat
R 石像鬼 灰色 Rock golem
A 暗影刺客 深紫 Assassin
F 火焰魔 亮红 Fire demon
D 深红 Dragon
· 地板 灰色 可通行区域
墙壁 棕色 不可通行
楼梯 金色 通往下一层
回血 粉色 恢复 HP
武器 橙色 永久 +ATK
🛡 盔甲 蓝色 永久 +DEF

八、构建验证

hvigorw assembleHap --daemon=false --analyze=false

构建输出:

Finished :entry:default@CompileArkTS... after 2 s 338 ms
Finished :entry:default@PackageHap... after 667 ms
Finished :entry:default@PackingCheck... after 4 ms
BUILD SUCCESSFUL in 4 s 234 ms

编译问题修复记录

在开发过程中修复了两个 ArkTS 特有的编译错误:

错误 原因 修复
arkts-no-obj-literals-as-types 使用内联对象类型 {x:number,y:number,w:number,h:number}[] 抽取为 interface Room
Only UI component syntax @Builder 中声明了 const ratio = ... 抽取为 getXpPercent() 方法

九、扩展方向

9.1 更多敌人和 Boss

当前有 8 种敌人类型。可以增加第 10 层的 Boss 战:

// Boss 模板
const bossTemplate: EnemyTemplate = {
  name: '地牢魔王',
  symbol: 'B',
  color: '#FF0000',
  hpBase: 80,
  atkBase: 15,
  defBase: 8,
  xpReward: 50,
};

9.2 物品多样性

增加更多道具类型:魔法卷轴(透视地图)、炸弹(炸开墙壁)、金币(计分不消耗)等。

9.3 技能系统

玩家每升 2 级获得一个技能点,可以解锁特殊能力:

  • 冲刺:一次移动两格
  • 治愈:立即恢复 50% HP(冷却 3 层)
  • 强化:下一次攻击伤害 ×2

9.4 商店楼层

每 3 层出现一个商店,玩家可以用击杀数兑换道具。

9.5 数据持久化

使用 @kit.ArkDataPreferences API 保存最佳战绩:

async function saveBestScore(context: Context, floor: number, kills: number) {
  const pref = await preferences.getPreferences(context, 'roguelikeData');
  await pref.put('bestFloor', floor);
  await pref.put('bestKills', kills);
  await pref.flush();
}

十、总结

本文通过一个完整的地牢探险 Roguelike 游戏,展示了在 HarmonyOS NEXT 上使用 ArkTS 开发回合制游戏的全流程。从程序化地图生成、回合制战斗系统到角色成长曲线,游戏虽小(705 行),但包含了 Roguelike 游戏的核心要素。

核心技术点回顾

技术点 应用位置 关键实现
二维数组网格 地图渲染 grid[y][x] 三层独立网格
程序化生成 carveRooms() 房间+走廊算法
回合制战斗 battle() 先攻→反击→死亡判定
伤害公式 battle() max(1, ATK - DEF + random)
难度缩放 generateFloor() scale = 1 + (floor-1) × 0.3
状态驱动 UI 全部 18 个 @State 驱动所有界面
条件渲染 弹窗 if(gameOver) buildOverlay()
网格渲染 buildMapArea() 嵌套 ForEach 遍历二维数组

ArkTS 开发 Roguelike 游戏的优势

  1. 声明式 UI 天然适合网格渲染ForEach 嵌套可以优雅地渲染二维地图
  2. @State 一键驱动游戏状态:修改 HP、位置等变量后 UI 自动同步
  3. 条件渲染简化弹窗逻辑:游戏结束/胜利弹窗通过 if 条件直接渲染
  4. 严格类型安全:接口定义杜绝了地图数据格式错误

Roguelike 游戏虽然玩法经典,但其在移动端通过触摸操作和简洁 UI 可以焕发新的生机。希望本文能为你在 HarmonyOS NEXT 上开发游戏类应用提供有价值的参考。

Logo

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

更多推荐