应用概述
森林冰火人(Fireboy and Watergirl)是一款经典的双角色解谜游戏,玩家需要同时控制火男孩和水女孩,利用各自特性通过关卡。
开发步骤
1. 创建项目
- 打开CodeGenie IDE
- 选择"File" > "New" > "HarmonyOS Project"
- 输入项目名称"FireboyWatergirl"
- 选择API版本为HarmonyOS 5
- 点击"Finish"
2. 准备游戏资源
在resources/base/media
目录下添加以下资源:
- 角色精灵图:
fireboy.png
,watergirl.png
- 地形元素:
ground.png
,water.png
,lava.png
,door.png
- 特效:
splash.png
,steam.png
- 背景音乐和音效
3. 设计游戏数据结构
在resources/base/profile
目录下创建game_config.json
:
{
"characters": {
"fireboy": {
"speed": 5,
"jumpForce": 10,
"canWalkOn": ["ground", "lava"],
"diesIn": ["water"],
"color": "#FF4500",
"image": "media/fireboy.png"
},
"watergirl": {
"speed": 5,
"jumpForce": 10,
"canWalkOn": ["ground", "water"],
"diesIn": ["lava"],
"color": "#1E90FF",
"image": "media/watergirl.png"
}
},
"levels": [
{
"id": 1,
"name": "森林神殿",
"map": [
"GGGGGGGGGGGGGGGGGGGG",
"G G",
"G F W L G",
"G D D G",
"G P P G",
"GGGGGGGGGGGGGGGGGGGG"
],
"legend": {
"G": {"type": "ground", "image": "media/ground.png"},
" ": {"type": "air"},
"F": {"type": "fire_door", "image": "media/door.png"},
"W": {"type": "water_door", "image": "media/door.png"},
"L": {"type": "lever", "image": "media/lever.png"},
"D": {"type": "diamond", "image": "media/diamond.png"},
"P": {"type": "platform", "image": "media/platform.png", "movable": true}
}
}
]
}
4. 创建游戏主界面
在resources/base/layout
目录下创建game_page.xml
:
<DirectionalLayout
xmlns:ohos="http://schemas.huawei.com/res/ohos"
ohos:width="match_parent"
ohos:height="match_parent"
ohos:orientation="vertical">
<Canvas
ohos:id="$+id:game_canvas"
ohos:width="match_parent"
ohos:height="match_parent"/>
<DirectionalLayout
ohos:width="match_parent"
ohos:height="60vp"
ohos:orientation="horizontal"
ohos:background_element="#80000000"
ohos:alignment="bottom">
<Button
ohos:id="$+id:left_btn"
ohos:width="80vp"
ohos:height="50vp"
ohos:text="←"
ohos:margin="5vp"/>
<Button
ohos:id="$+id:right_btn"
ohos:width="80vp"
ohos:height="50vp"
ohos:text="→"
ohos:margin="5vp"/>
<Button
ohos:id="$+id:jump_btn"
ohos:width="80vp"
ohos:height="50vp"
ohos:text="跳跃"
ohos:margin="5vp"/>
<Button
ohos:id="$+id:switch_btn"
ohos:width="80vp"
ohos:height="50vp"
ohos:text="切换"
ohos:margin="5vp"/>
</DirectionalLayout>
</DirectionalLayout>
5. 实现游戏引擎
在src/main/ets/MainAbility
目录下创建GameEngine.ets
:
import gameConfig from '../../resources/base/profile/game_config.json';
import { Character, Level, GameMap, GameObject } from '../common/model';
export class GameEngine {
private canvas: CanvasRenderingContext2D | null = null;
private characters: Record<string, Character> = {};
private currentLevel: Level;
private gameMap: GameMap = [];
private activeCharacter: string = 'fireboy';
private gameObjects: GameObject[] = [];
private lastTime: number = 0;
private isRunning: boolean = false;
constructor(canvas: CanvasRenderingContext2D, level: Level) {
this.canvas = canvas;
this.currentLevel = level;
this.initCharacters();
this.parseLevelMap();
}
private initCharacters() {
this.characters = {
fireboy: {
...gameConfig.characters.fireboy,
x: 100,
y: 300,
width: 40,
height: 60,
velocityX: 0,
velocityY: 0,
isJumping: false,
collectedDiamonds: 0
},
watergirl: {
...gameConfig.characters.watergirl,
x: 150,
y: 300,
width: 40,
height: 60,
velocityX: 0,
velocityY: 0,
isJumping: false,
collectedDiamonds: 0
}
};
}
private parseLevelMap() {
const { map, legend } = this.currentLevel;
const tileSize = 40;
for (let y = 0; y < map.length; y++) {
const row = map[y];
this.gameMap[y] = [];
for (let x = 0; x < row.length; x++) {
const tile = row[x];
const tileConfig = legend[tile];
if (tileConfig) {
this.gameMap[y][x] = {
...tileConfig,
x: x * tileSize,
y: y * tileSize,
width: tileSize,
height: tileSize
};
if (tileConfig.type !== 'air') {
this.gameObjects.push(this.gameMap[y][x]);
}
}
}
}
}
start() {
this.isRunning = true;
this.lastTime = Date.now();
this.gameLoop();
}
stop() {
this.isRunning = false;
}
private gameLoop() {
if (!this.isRunning || !this.canvas) return;
const now = Date.now();
const deltaTime = (now - this.lastTime) / 1000;
this.lastTime = now;
this.update(deltaTime);
this.render();
requestAnimationFrame(() => this.gameLoop());
}
private update(deltaTime: number) {
// 更新角色位置
Object.values(this.characters).forEach(character => {
// 应用重力
character.velocityY += 20 * deltaTime;
// 更新位置
character.x += character.velocityX * deltaTime;
character.y += character.velocityY * deltaTime;
// 检测碰撞
this.checkCollisions(character);
});
// 检查游戏状态
this.checkGameStatus();
}
private checkCollisions(character: Character) {
let onGround = false;
this.gameObjects.forEach(obj => {
if (this.isColliding(character, obj)) {
// 处理不同类型的碰撞
switch (obj.type) {
case 'ground':
case 'platform':
this.handleGroundCollision(character, obj);
onGround = true;
break;
case 'water':
if (character.canWalkOn.includes('water')) {
this.handleWaterCollision(character, obj);
} else {
// 火男孩遇水死亡
this.characters.fireboy = this.resetCharacter(this.characters.fireboy);
}
break;
case 'lava':
if (character.canWalkOn.includes('lava')) {
this.handleLavaCollision(character, obj);
} else {
// 水女孩遇岩浆死亡
this.characters.watergirl = this.resetCharacter(this.characters.watergirl);
}
break;
case 'diamond':
this.handleDiamondCollision(character, obj);
break;
case 'fire_door':
case 'water_door':
this.handleDoorCollision(character, obj);
break;
case 'lever':
this.handleLeverCollision(character, obj);
break;
}
}
});
character.isJumping = !onGround;
}
private isColliding(a: GameObject, b: GameObject): boolean {
return a.x < b.x + b.width &&
a.x + a.width > b.x &&
a.y < b.y + b.height &&
a.y + a.height > b.y;
}
private handleGroundCollision(character: Character, ground: GameObject) {
// 简单的碰撞响应 - 将角色放在地面之上
character.y = ground.y - character.height;
character.velocityY = 0;
}
private resetCharacter(character: Character): Character {
// 重置角色到起始位置
return {
...character,
x: character === this.characters.fireboy ? 100 : 150,
y: 300,
velocityX: 0,
velocityY: 0,
isJumping: false
};
}
private checkGameStatus() {
// 检查是否两个角色都到达了各自的门
const fireboy = this.characters.fireboy;
const watergirl = this.characters.watergirl;
const fireboyAtDoor = this.gameObjects.some(obj =>
obj.type === 'fire_door' && this.isColliding(fireboy, obj)
);
const watergirlAtDoor = this.gameObjects.some(obj =>
obj.type === 'water_door' && this.isColliding(watergirl, obj)
);
if (fireboyAtDoor && watergirlAtDoor) {
this.stop();
prompt.showToast({ message: '关卡完成!', duration: 2000 });
}
}
private render() {
if (!this.canvas) return;
// 清空画布
this.canvas.clearRect(0, 0, this.canvas.width, this.canvas.height);
// 绘制地图
this.gameObjects.forEach(obj => {
if (obj.image) {
const img = new Image();
img.src = obj.image;
this.canvas?.drawImage(img, obj.x, obj.y, obj.width, obj.height);
}
});
// 绘制角色
Object.entries(this.characters).forEach(([name, character]) => {
const img = new Image();
img.src = character.image;
this.canvas?.drawImage(img, character.x, character.y, character.width, character.height);
// 高亮显示当前活动角色
if (name === this.activeCharacter) {
this.canvas.strokeStyle = '#FFFFFF';
this.canvas.lineWidth = 2;
this.canvas.strokeRect(character.x, character.y, character.width, character.height);
}
});
}
// 控制方法
moveCharacter(direction: 'left' | 'right') {
const character = this.characters[this.activeCharacter];
const speed = character.speed;
character.velocityX = direction === 'left' ? -speed : speed;
}
stopCharacter() {
this.characters[this.activeCharacter].velocityX = 0;
}
jumpCharacter() {
const character = this.characters[this.activeCharacter];
if (!character.isJumping) {
character.velocityY = -character.jumpForce;
character.isJumping = true;
}
}
switchCharacter() {
this.activeCharacter = this.activeCharacter === 'fireboy' ? 'watergirl' : 'fireboy';
}
}
6. 实现游戏页面
创建GamePage.ets
:
import { GameEngine } from './GameEngine';
import gameConfig from '../../resources/base/profile/game_config.json';
import { Level } from '../common/model';
@Entry
@Component
struct GamePage {
private canvasRef: CanvasRenderingContext2D | null = null;
private gameEngine: GameEngine | null = null;
private currentLevel: Level = gameConfig.levels[0];
onPageShow() {
if (this.canvasRef) {
this.gameEngine = new GameEngine(this.canvasRef, this.currentLevel);
this.gameEngine.start();
}
}
onPageHide() {
if (this.gameEngine) {
this.gameEngine.stop();
}
}
build() {
Column() {
Canvas(this.canvasRef)
.width('100%')
.height('80%')
.onReady((canvas: CanvasRenderingContext2D) => {
this.canvasRef = canvas;
})
Row() {
Button('←')
.width(80)
.height(50)
.margin(5)
.onTouch((event: TouchEvent) => {
if (event.type === TouchType.Down) {
this.gameEngine?.moveCharacter('left');
} else if (event.type === TouchType.Up) {
this.gameEngine?.stopCharacter();
}
})
Button('→')
.width(80)
.height(50)
.margin(5)
.onTouch((event: TouchEvent) => {
if (event.type === TouchType.Down) {
this.gameEngine?.moveCharacter('right');
} else if (event.type === TouchType.Up) {
this.gameEngine?.stopCharacter();
}
})
Button('跳跃')
.width(80)
.height(50)
.margin(5)
.onClick(() => {
this.gameEngine?.jumpCharacter();
})
Button('切换')
.width(80)
.height(50)
.margin(5)
.onClick(() => {
this.gameEngine?.switchCharacter();
})
}
.width('100%')
.height(60)
.justifyContent(FlexAlign.SpaceAround)
}
}
}
7. 创建关卡选择界面
在resources/base/layout
目录下创建level_select_page.xml
:
<DirectionalLayout
xmlns:ohos="http://schemas.huawei.com/res/ohos"
ohos:width="match_parent"
ohos:height="match_parent"
ohos:orientation="vertical"
ohos:padding="20vp">
<Text
ohos:width="match_parent"
ohos:height="wrap_content"
ohos:text="选择关卡"
ohos:text_size="24fp"
ohos:text_alignment="center"
ohos:margin="20vp"/>
<GridLayout
ohos:id="$+id:level_grid"
ohos:width="match_parent"
ohos:height="match_parent"
ohos:column_count="3"
ohos:row_count="2"/>
</DirectionalLayout>
8. 实现关卡选择逻辑
创建LevelSelectPage.ets
:
import gameConfig from '../../resources/base/profile/game_config.json';
import { Level } from '../common/model';
@Entry
@Component
struct LevelSelectPage {
@State levels: Array<Level> = gameConfig.levels;
build() {
Column() {
Text('选择关卡')
.fontSize(24)
.textAlign(TextAlign.Center)
.margin(20)
Grid() {
ForEach(this.levels, (level: Level) => {
GridItem() {
Column() {
Image($r('app.media.level_icon'))
.width(60)
.height(60)
.margin(10)
Text(level.name)
.fontSize(16)
.margin({ bottom: 10 })
}
.width('90%')
.height(120)
.borderRadius(10)
.backgroundColor('#333333')
.onClick(() => {
router.push({
url: 'pages/GamePage',
params: { levelId: level.id }
})
})
}
})
}
.columnsTemplate('1fr 1fr 1fr')
.rowsTemplate('1fr 1fr')
.width('100%')
.height('80%')
}
}
}
9. 添加类型定义
在src/main/ets/common
目录下创建model.ets
:
export interface Character {
x: number;
y: number;
width: number;
height: number;
speed: number;
jumpForce: number;
canWalkOn: string[];
diesIn: string[];
color: string;
image: string;
velocityX: number;
velocityY: number;
isJumping: boolean;
collectedDiamonds: number;
}
export interface GameObject {
type: string;
x: number;
y: number;
width: number;
height: number;
image?: string;
movable?: boolean;
}
export interface Level {
id: number;
name: string;
map: string[];
legend: Record<string, GameObject>;
}
export interface GameMap {
[y: number]: GameObject[];
}
10. 修改入口页面
修改src/main/ets/MainAbility
目录下的MainAbility.ts
:
export default class MainAbility extends Ability {
onWindowStageCreate(windowStage: window.WindowStage) {
windowStage.loadContent('pages/LevelSelectPage', (err, data) => {
if (err.code) {
console.error('Failed to load the content. Cause:' + JSON.stringify(err));
return;
}
console.info('Succeeded in loading the content. Data: ' + JSON.stringify(data));
});
}
}
功能扩展建议
- 更多关卡设计:添加更多精心设计的关卡
- 角色动画:添加行走、跳跃等动画效果
- 特效系统:水花、蒸汽等粒子效果
- 音效系统:背景音乐和游戏音效
- 存档系统:保存游戏进度
- 多人模式:支持两个玩家同时操作
- 编辑器功能:允许玩家创建自定义关卡
- 成就系统:完成特定挑战解锁成就
测试与发布
- 在CodeGenie中使用预览器测试游戏界面
- 测试角色移动和碰撞检测
- 测试关卡切换和游戏逻辑
- 在真机设备上测试触摸控制
- 使用HUAWEI AppGallery Connect进行应用签名和发布
这个森林冰火人小游戏将充分利用鸿蒙系统的图形渲染能力和触摸交互,为玩家带来有趣的解谜体验。通过双角色协作机制,培养玩家的逻辑思维和协作能力。