# 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;
}
}
}
算法步骤:
- 决定 3-5 个房间的数量和位置(确保不越界)
- 在每个房间位置,将墙体替换为地板
- 将房间中心两两相连,形成 L 形走廊
- 未挖通的区域保持为墙壁
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.ArkData 的 Preferences 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 游戏的优势
- 声明式 UI 天然适合网格渲染:
ForEach嵌套可以优雅地渲染二维地图 - @State 一键驱动游戏状态:修改 HP、位置等变量后 UI 自动同步
- 条件渲染简化弹窗逻辑:游戏结束/胜利弹窗通过
if条件直接渲染 - 严格类型安全:接口定义杜绝了地图数据格式错误
Roguelike 游戏虽然玩法经典,但其在移动端通过触摸操作和简洁 UI 可以焕发新的生机。希望本文能为你在 HarmonyOS NEXT 上开发游戏类应用提供有价值的参考。
更多推荐


所有评论(0)