Flutter for OpenHarmony从零到一:构建《冰火人》双人合作闯关游戏
在移动游戏开发领域,将经典玩法与现代框架相结合,是激发创造力的绝佳方式。本文将深入剖析一段使用 Flutter
框架实现的《冰火人》风格双人合作闯关游戏的完整源码。我们将逐层拆解其架构设计、核心逻辑与交互细节,揭示如何用 Dart
语言构建一个功能完备、体验流畅的 2D 平台游戏。
完整效果展示




一、整体架构:清晰的页面导航与状态管理
游戏采用了经典的 三层页面结构,确保了逻辑的清晰分离:
MainMenu(主菜单): 游戏的入口,提供"开始游戏"和"游戏说明"选项。LevelSelectScreen(关卡选择): 以网格形式展示所有可玩关卡。GameScreen(游戏主界面) : 核心的游戏逻辑和渲染都在此StatefulWidget中完成。
这种结构使得代码易于维护和扩展。例如,要添加新关卡,只需在 LevelSelectScreen 的 GridView 中增加一项,并在 GameScreen 的 _loadLevel 方法中实现新的关卡逻辑即可。
关键代码:页面导航
dart
// 从主菜单跳转到关卡选择
Navigator.push(context, MaterialPageRoute(builder: (context) => const LevelSelectScreen()));
// 从关卡选择进入具体游戏
Navigator.push(context, MaterialPageRoute(builder: (context) => GameScreen(level: level)));
Flutter 的 Navigator 提供了简洁的路由管理,实现了页面间的无缝切换。
二、数据模型:定义游戏世界的基石
游戏世界由一系列自定义的 Dart 类 (Platform, LavaPool, WaterPool, Gem) 构成。这些简单的数据模型是整个游戏逻辑的基石。
dart
class Platform {
final int x, y, width, height; // 平台的位置和尺寸
const Platform({required this.x, ...});
}
class Gem {
final double x, y; // 宝石位置(使用double以支持更精细的定位)
final Color color; // 宝石颜色,用于区分归属(红/蓝/绿)
const Gem({...});
}

设计亮点:
- 类型安全:每个游戏元素都有明确的属性,避免了使用 Map 或 List 存储数据带来的类型混乱和潜在错误。
- 可读性强 :
platform.x比gameData[0]更直观,极大地提升了代码的可读性和可维护性。
三、核心引擎:游戏循环与物理模拟
GameScreen 的 State 类是整个游戏的"心脏",它通过一个 定时器驱动的游戏循环 来更新世界状态。
3.1 游戏循环 (_startGame)
dart
void _startGame() {
_gameTimer = Timer.periodic(const Duration(milliseconds: 16), (timer) {
if (!_isPaused && !_gameOver && !_gameWin) {
_updateGame(); // 核心更新逻辑
}
});
}

- 帧率控制 :
Duration(milliseconds: 16)对应约 60 FPS,保证了流畅的动画效果。- 状态检查:循环只在游戏进行中(非暂停、非结束)时执行,节省了系统资源。
3.2 物理与移动 (_updateGame)
这是最核心的逻辑,每帧都会执行:
- 处理输入 :根据键盘或虚拟按键的状态(如
_leftPressed),设置玩家角色的水平速度 (_fireboyVelocityX)。 - 应用重力 :持续给垂直速度 (
_velocityY) 增加一个值(+= 0.5),模拟重力下落效果。 - 更新位置:将速度向量加到当前位置上。
- 碰撞检测:这是平台游戏的灵魂,代码中实现了三种关键碰撞:
- 平台碰撞 (
_checkPlatformCollision): 判断角色是否站在平台上(用于跳跃)或撞到平台侧面/底部。
- 危险区域碰撞 (
_checkDangerZones): 检测火焰人是否掉入水池或冰人是否掉入岩浆,触发游戏结束。- 宝石收集 (
_collectGems): 检测角色是否触碰到宝石,并根据颜色(红/蓝/绿)增加对应玩家的分数。- 门碰撞 (
_checkDoorCollision): 检测两名角色是否同时到达各自的终点门,触发过关。
3.3 碰撞检测详解
以平台碰撞为例,代码不仅检测了重叠 (overlaps),还通过判断 速度方向 和 碰撞位置 来区分是从上方落下(需要站在上面)还是从下方/侧面撞上(需要被阻挡)。
dart
// 从上方落下
if (_fireboyVelocityY > 0 && position.dy + _playerSize.height <= platformRect.top + 10) {
// 将角色位置精确调整到平台顶部
_fireboyPosition = Offset(position.dx, platformRect.top - _playerSize.height);
_fireboyOnGround = true; // 允许再次跳跃
_fireboyVelocityY = 0; // 垂直速度归零
}
这种精细化的处理是实现流畅、符合直觉的平台跳跃体验的关键。
四、用户交互:跨平台的输入适配
为了让游戏在桌面和移动端都能玩,代码巧妙地实现了 双重输入方案。
4.1 键盘输入 (桌面端)
通过
RawKeyboardListener(虽然代码中未显式写出,但通常配合FocusNode使用)或全局键盘事件监听,可以捕获
WASD和IJKL等按键,并更新对应的布尔状态(如_leftPressed = true)。这些状态会在_updateGame中被读取,从而控制角色移动。
4.2 虚拟按键 (移动端)
对于触摸屏设备,游戏提供了精美的 虚拟摇杆。
dart
GestureDetector(
onTapDown: (_) => setState(() => _watergirlJumpPressed = true),
onTapUp: (_) => setState(() => _watergirlJumpPressed = false),
child: Container(...), // 绘制一个圆形按钮
)

onTapDown/onTapUp: 这两个回调完美模拟了物理按键的"按下"和"松开"状态。- 视觉反馈: 按钮使用了半透明的蓝色/橙色,并带有白色边框和方向图标,清晰地指示了其功能和所属角色。
此外,游戏还贴心地加入了 显示/隐藏控制 的功能,玩家点击屏幕任意空白处即可切换虚拟按键的可见性,避免遮挡视线。
五、UI 与视觉呈现:沉浸式的游戏体验
Flutter 强大的 Stack 和 Positioned 组件是构建 2D 游戏 UI 的利器。
5.1 游戏世界渲染
整个游戏区域是一个 Stack,所有游戏元素(平台、岩浆、水池、宝石、门、玩家角色)都作为 Positioned 子组件被精确地放置在指定坐标上。
dart
Stack(
children: [
..._platforms.map((p) => Positioned(left: p.x, top: p.y, child: ...)),
..._gems.map((g) => Positioned(left: g.x, top: g.y, child: ...)),
Positioned(left: _fireboyPosition.dx, top: _fireboyPosition.dy, child: ...),
],
)
这种"绝对定位"的方式非常适合 2D 游戏的坐标系。
5.2 角色与环境设计
- 角色 :火焰人和冰人分别用
LinearGradient渲染出橙红色和蓝白色的渐变效果,并辅以BoxShadow制造发光感,再配上local_fire_department和ac_unit图标,形象生动。 - 环境 :岩浆 (
LavaPool) 和水池 (WaterPool) 同样使用渐变色 (red->orange->yellow和lightBlue->blue->darkBlue),营造出逼真的视觉效果。 - 宝石:不同颜色的圆形宝石带有同色光晕,非常醒目。
5.3 状态覆盖层
游戏通过条件渲染 (if (_gameOver) ...) 在 Stack 顶部动态添加覆盖层,用于显示 暂停、游戏结束、胜利 等状态。这种方式简单高效,且能完全屏蔽底层的游戏交互。
六、关卡设计:数据驱动的可扩展性
游戏的关卡逻辑完全由 _loadLevel1/2/3 等方法中的 数据初始化 决定。
dart
void _loadLevel1() {
_fireboyPosition = const Offset(50, 400);
_platforms = [const Platform(x: 0, y: 480, ...), ...];
_lavaPools = [const LavaPool(x: 150, y: 460, ...)];
...
}
这种 数据驱动 的设计模式意味着:
- 易于修改:设计师可以直接调整数字来改变关卡布局。
- 易于扩展:未来可以轻松地将关卡数据从 JSON 文件或网络加载,而无需改动核心游戏逻辑。
总结
这段《冰火人》游戏代码是一个教科书式的 Flutter 2D 游戏开发范例。它展示了如何利用 Flutter 的声明式 UI、强大的状态管理和丰富的 Widget 生态,来构建一个包含 复杂物理模拟、精细碰撞检测、跨平台输入适配和精美视觉效果 的完整游戏。
🌐 加入社区
欢迎加入 开源鸿蒙跨平台开发者社区 ,获取最新资源与技术支持:
👉 开源鸿蒙跨平台开发者社区
完整代码展示
bash
import 'package:flutter/material.dart';
import 'dart:async';
void main() {
runApp(const FireboyWatergirlGame());
}
class FireboyWatergirlGame extends StatelessWidget {
const FireboyWatergirlGame({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '冰火人闯关',
theme: ThemeData(
primarySwatch: Colors.orange,
useMaterial3: true,
brightness: Brightness.dark,
),
home: const MainMenu(),
debugShowCheckedModeBanner: false,
);
}
}
class MainMenu extends StatelessWidget {
const MainMenu({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.red.withOpacity(0.6), Colors.blue.withOpacity(0.6)],
),
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.extension,
size: 120,
color: Colors.white,
),
const SizedBox(height: 20),
const Text(
'冰火人闯关',
style: TextStyle(
fontSize: 48,
fontWeight: FontWeight.bold,
color: Colors.white,
shadows: [
Shadow(
color: Colors.black,
blurRadius: 10,
),
],
),
),
const SizedBox(height: 60),
ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 60, vertical: 20),
textStyle: const TextStyle(fontSize: 24),
backgroundColor: Colors.green,
foregroundColor: Colors.white,
),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const LevelSelectScreen(),
),
);
},
child: const Text('开始游戏'),
),
const SizedBox(height: 20),
ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 60, vertical: 20),
textStyle: const TextStyle(fontSize: 24),
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
),
onPressed: () {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('游戏说明'),
content: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Text('🔥 火焰人操作:', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.orange)),
const Text('• WASD 或方向键移动'),
const Text('• A/D: 左右移动'),
const Text('• W: 跳跃'),
const SizedBox(height: 16),
const Text('💧 冰人操作:', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.blue)),
const Text('• J/L: 左右移动'),
const Text('• I: 跳跃'),
const SizedBox(height: 16),
const Text('🎯 游戏目标:', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.yellow)),
const Text('• 火焰人到达红色门'),
const Text('• 冰人到达蓝色门'),
const Text('• 两人必须同时到达才能过关'),
const SizedBox(height: 16),
const Text('⚠️ 注意:', style: TextStyle(fontWeight: FontWeight.bold)),
const Text('• 火焰人碰到水会死亡'),
const Text('• 冰人碰到岩浆会死亡'),
const Text('• 任意一人死亡则游戏结束'),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('知道了'),
),
],
),
);
},
child: const Text('游戏说明'),
),
],
),
),
),
);
}
}
class LevelSelectScreen extends StatelessWidget {
const LevelSelectScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('选择关卡'),
centerTitle: true,
),
body: GridView.builder(
padding: const EdgeInsets.all(16),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 1,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
),
itemCount: 9,
itemBuilder: (context, index) {
int level = index + 1;
return GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => GameScreen(level: level),
),
);
},
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Colors.red, Colors.blue],
),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Center(
child: Text(
'$level',
style: const TextStyle(
fontSize: 48,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
),
);
},
),
);
}
}
class GameScreen extends StatefulWidget {
final int level;
const GameScreen({super.key, required this.level});
@override
State<GameScreen> createState() => _GameState();
}
class _GameState extends State<GameScreen> {
late Timer _gameTimer;
bool _isPaused = false;
bool _gameOver = false;
bool _gameWin = false;
String _deathReason = '';
// 游戏状态
Offset _fireboyPosition = const Offset(50, 300);
Offset _watergirlPosition = const Offset(250, 300);
Size _playerSize = const Size(30, 40);
double _fireboyVelocityX = 0;
double _fireboyVelocityY = 0;
double _watergirlVelocityX = 0;
double _watergirlVelocityY = 0;
bool _fireboyOnGround = false;
bool _watergirlOnGround = false;
// 关卡元素
List<Platform> _platforms = [];
List<LavaPool> _lavaPools = [];
List<WaterPool> _waterPools = [];
List<Gem> _gems = [];
// 门的位置
late Offset _redDoorPosition;
late Offset _blueDoorPosition;
bool _fireboyAtDoor = false;
bool _watergirlAtDoor = false;
// 收集统计
int _fireboyGems = 0;
int _watergirlGems = 0;
int _totalFireboyGems = 0;
int _totalWatergirlGems = 0;
// 按键状态
bool _leftPressed = false;
bool _rightPressed = false;
bool _fireboyJumpPressed = false;
bool _watergirlLeftPressed = false;
bool _watergirlRightPressed = false;
bool _watergirlJumpPressed = false;
// 移动端虚拟按键
bool _showControls = true;
@override
void initState() {
super.initState();
_loadLevel();
_startGame();
}
void _loadLevel() {
// 根据关卡号加载不同的关卡设计
switch (widget.level) {
case 1:
_loadLevel1();
break;
case 2:
_loadLevel2();
break;
case 3:
_loadLevel3();
break;
default:
_loadLevel1();
}
}
void _loadLevel1() {
_fireboyPosition = const Offset(50, 400);
_watergirlPosition = const Offset(100, 400);
// 平台
_platforms = [
const Platform(x: 0, y: 480, width: 400, height: 20), // 地面
const Platform(x: 0, y: 350, width: 150, height: 20),
const Platform(x: 200, y: 350, width: 200, height: 20),
const Platform(x: 0, y: 220, width: 120, height: 20),
const Platform(x: 180, y: 220, width: 220, height: 20),
const Platform(x: 300, y: 100, width: 100, height: 20),
];
// 岩浆和水池
_lavaPools = [
const LavaPool(x: 150, y: 460, width: 50, height: 20),
];
_waterPools = [
const WaterPool(x: 250, y: 460, width: 50, height: 20),
];
// 宝石
_gems = [
const Gem(x: 75, y: 300, color: Colors.red),
const Gem(x: 300, y: 300, color: Colors.blue),
const Gem(x: 60, y: 170, color: Colors.red),
const Gem(x: 300, y: 170, color: Colors.blue),
];
_totalFireboyGems = 2;
_totalWatergirlGems = 2;
// 门
_redDoorPosition = const Offset(320, 50);
_blueDoorPosition = const Offset(250, 50);
}
void _loadLevel2() {
_fireboyPosition = const Offset(30, 400);
_watergirlPosition = const Offset(80, 400);
_platforms = [
const Platform(x: 0, y: 480, width: 400, height: 20),
const Platform(x: 0, y: 380, width: 100, height: 20),
const Platform(x: 150, y: 380, width: 100, height: 20),
const Platform(x: 300, y: 380, width: 100, height: 20),
const Platform(x: 50, y: 280, width: 100, height: 20),
const Platform(x: 250, y: 280, width: 100, height: 20),
const Platform(x: 150, y: 180, width: 100, height: 20),
const Platform(x: 50, y: 100, width: 80, height: 20),
const Platform(x: 270, y: 100, width: 80, height: 20),
];
_lavaPools = [
const LavaPool(x: 100, y: 460, width: 50, height: 20),
const LavaPool(x: 250, y: 460, width: 50, height: 20),
];
_waterPools = [
const WaterPool(x: 200, y: 340, width: 50, height: 20),
];
_gems = [
const Gem(x: 50, y: 340, color: Colors.red),
const Gem(x: 350, y: 340, color: Colors.blue),
const Gem(x: 100, y: 240, color: Colors.red),
const Gem(x: 300, y: 240, color: Colors.blue),
const Gem(x: 200, y: 140, color: Colors.green),
];
_totalFireboyGems = 3;
_totalWatergirlGems = 2;
_redDoorPosition = const Offset(60, 50);
_blueDoorPosition = const Offset(290, 50);
}
void _loadLevel3() {
_fireboyPosition = const Offset(30, 400);
_watergirlPosition = const Offset(70, 400);
_platforms = [
const Platform(x: 0, y: 480, width: 400, height: 20),
const Platform(x: 0, y: 380, width: 80, height: 20),
const Platform(x: 120, y: 350, width: 80, height: 20),
const Platform(x: 240, y: 320, width: 80, height: 20),
const Platform(x: 320, y: 380, width: 80, height: 20),
const Platform(x: 50, y: 260, width: 80, height: 20),
const Platform(x: 170, y: 230, width: 80, height: 20),
const Platform(x: 290, y: 260, width: 80, height: 20),
const Platform(x: 120, y: 160, width: 80, height: 20),
const Platform(x: 200, y: 100, width: 100, height: 20),
];
_lavaPools = [
const LavaPool(x: 80, y: 460, width: 60, height: 20),
const LavaPool(x: 200, y: 460, width: 60, height: 20),
const LavaPool(x: 300, y: 340, width: 50, height: 20),
];
_waterPools = [
const WaterPool(x: 280, y: 460, width: 70, height: 20),
const WaterPool(x: 100, y: 220, width: 50, height: 20),
];
_gems = [
const Gem(x: 40, y: 340, color: Colors.red),
const Gem(x: 160, y: 310, color: Colors.red),
const Gem(x: 280, y: 280, color: Colors.blue),
const Gem(x: 90, y: 220, color: Colors.red),
const Gem(x: 210, y: 190, color: Colors.blue),
const Gem(x: 330, y: 220, color: Colors.blue),
const Gem(x: 160, y: 120, color: Colors.green),
const Gem(x: 250, y: 60, color: Colors.green),
];
_totalFireboyGems = 4;
_totalWatergirlGems = 3;
_redDoorPosition = const Offset(210, 50);
_blueDoorPosition = const Offset(260, 50);
}
void _startGame() {
_gameTimer = Timer.periodic(const Duration(milliseconds: 16), (timer) {
if (!_isPaused && !_gameOver && !_gameWin) {
_updateGame();
}
});
}
void _updateGame() {
// 火焰人移动
if (_leftPressed) {
_fireboyVelocityX = -4;
} else if (_rightPressed) {
_fireboyVelocityX = 4;
} else {
_fireboyVelocityX = 0;
}
if (_fireboyJumpPressed && _fireboyOnGround) {
_fireboyVelocityY = -12;
_fireboyOnGround = false;
}
// 冰人移动
if (_watergirlLeftPressed) {
_watergirlVelocityX = -4;
} else if (_watergirlRightPressed) {
_watergirlVelocityX = 4;
} else {
_watergirlVelocityX = 0;
}
if (_watergirlJumpPressed && _watergirlOnGround) {
_watergirlVelocityY = -12;
_watergirlOnGround = false;
}
// 应用重力
_fireboyVelocityY += 0.5;
_watergirlVelocityY += 0.5;
// 更新位置
_fireboyPosition += Offset(_fireboyVelocityX, _fireboyVelocityY);
_watergirlPosition += Offset(_watergirlVelocityX, _watergirlVelocityY);
// 边界检测
_fireboyPosition = Offset(
_fireboyPosition.dx.clamp(0, 370),
_fireboyPosition.dy.clamp(0, 440),
);
_watergirlPosition = Offset(
_watergirlPosition.dx.clamp(0, 370),
_watergirlPosition.dy.clamp(0, 440),
);
// 平台碰撞检测
_fireboyOnGround = false;
_watergirlOnGround = false;
for (var platform in _platforms) {
_checkPlatformCollision(_fireboyPosition, platform, true);
_checkPlatformCollision(_watergirlPosition, platform, false);
}
// 危险区域检测
_checkDangerZones();
// 宝石收集
_collectGems();
// 检查是否到达门口
_checkDoorCollision();
setState(() {});
}
void _checkPlatformCollision(Offset position, Platform platform, bool isFireboy) {
Rect playerRect = Rect.fromLTWH(
position.dx,
position.dy,
_playerSize.width,
_playerSize.height,
);
Rect platformRect = Rect.fromLTWH(
platform.x.toDouble(),
platform.y.toDouble(),
platform.width.toDouble(),
platform.height.toDouble(),
);
if (playerRect.overlaps(platformRect)) {
// 从上方落下
if ((isFireboy ? _fireboyVelocityY : _watergirlVelocityY) > 0 &&
position.dy + _playerSize.height <= platformRect.top + 10) {
position = Offset(
position.dx,
platformRect.top - _playerSize.height,
);
if (isFireboy) {
_fireboyPosition = position;
_fireboyOnGround = true;
_fireboyVelocityY = 0;
} else {
_watergirlPosition = position;
_watergirlOnGround = true;
_watergirlVelocityY = 0;
}
}
// 从下方碰撞
else if ((isFireboy ? _fireboyVelocityY : _watergirlVelocityY) < 0 &&
position.dy >= platformRect.bottom - 10) {
position = Offset(position.dx, platformRect.bottom);
if (isFireboy) {
_fireboyPosition = position;
_fireboyVelocityY = 0;
} else {
_watergirlPosition = position;
_watergirlVelocityY = 0;
}
}
}
}
void _checkDangerZones() {
Rect fireboyRect = Rect.fromLTWH(
_fireboyPosition.dx,
_fireboyPosition.dy,
_playerSize.width,
_playerSize.height,
);
Rect watergirlRect = Rect.fromLTWH(
_watergirlPosition.dx,
_watergirlPosition.dy,
_playerSize.width,
_playerSize.height,
);
// 检查岩浆 - 火焰人安全,冰人危险
for (var lava in _lavaPools) {
Rect lavaRect = Rect.fromLTWH(
lava.x.toDouble(),
lava.y.toDouble(),
lava.width.toDouble(),
lava.height.toDouble(),
);
if (watergirlRect.overlaps(lavaRect)) {
_gameOver = true;
_deathReason = '冰人掉进了岩浆!';
return;
}
}
// 检查水池 - 冰人安全,火焰人危险
for (var water in _waterPools) {
Rect waterRect = Rect.fromLTWH(
water.x.toDouble(),
water.y.toDouble(),
water.width.toDouble(),
water.height.toDouble(),
);
if (fireboyRect.overlaps(waterRect)) {
_gameOver = true;
_deathReason = '火焰人掉进了水池!';
return;
}
}
}
void _collectGems() {
Rect fireboyRect = Rect.fromLTWH(
_fireboyPosition.dx + 5,
_fireboyPosition.dy + 5,
_playerSize.width - 10,
_playerSize.height - 10,
);
Rect watergirlRect = Rect.fromLTWH(
_watergirlPosition.dx + 5,
_watergirlPosition.dy + 5,
_playerSize.width - 10,
_playerSize.height - 10,
);
for (int i = _gems.length - 1; i >= 0; i--) {
Gem gem = _gems[i];
Rect gemRect = Rect.fromLTWH(gem.x.toDouble(), gem.y.toDouble(), 20, 20);
if (fireboyRect.overlaps(gemRect) && gem.color == Colors.red) {
_fireboyGems++;
_gems.removeAt(i);
} else if (watergirlRect.overlaps(gemRect) && gem.color == Colors.blue) {
_watergirlGems++;
_gems.removeAt(i);
} else if ((fireboyRect.overlaps(gemRect) || watergirlRect.overlaps(gemRect)) && gem.color == Colors.green) {
_fireboyGems++;
_watergirlGems++;
_gems.removeAt(i);
}
}
}
void _checkDoorCollision() {
Rect fireboyRect = Rect.fromLTWH(
_fireboyPosition.dx,
_fireboyPosition.dy,
_playerSize.width,
_playerSize.height,
);
Rect watergirlRect = Rect.fromLTWH(
_watergirlPosition.dx,
_watergirlPosition.dy,
_playerSize.width,
_playerSize.height,
);
Rect redDoorRect = Rect.fromLTWH(_redDoorPosition.dx, _redDoorPosition.dy, 40, 60);
Rect blueDoorRect = Rect.fromLTWH(_blueDoorPosition.dx, _blueDoorPosition.dy, 40, 60);
_fireboyAtDoor = fireboyRect.overlaps(redDoorRect);
_watergirlAtDoor = watergirlRect.overlaps(blueDoorRect);
if (_fireboyAtDoor && _watergirlAtDoor) {
_gameWin = true;
_gameTimer.cancel();
}
}
@override
void dispose() {
_gameTimer.cancel();
super.dispose();
}
void _restartLevel() {
setState(() {
_gameOver = false;
_gameWin = false;
_deathReason = '';
_fireboyGems = 0;
_watergirlGems = 0;
_gems.clear();
_loadLevel();
});
}
void _nextLevel() {
if (widget.level < 9) {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => GameScreen(level: widget.level + 1),
),
);
} else {
Navigator.pop(context);
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('恭喜通关!'),
content: const Text('你已完成所有关卡!'),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context);
Navigator.pop(context);
},
child: const Text('返回'),
),
],
),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[900],
body: Column(
children: [
// 顶部状态栏
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
color: Colors.black87,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
_gameTimer.cancel();
Navigator.pop(context);
},
),
Text(
'关卡 ${widget.level}',
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
],
),
Row(
children: [
const Icon(Icons.diamond, color: Colors.red, size: 20),
Text(' $_fireboyGems/$_totalFireboyGems'),
const SizedBox(width: 16),
const Icon(Icons.diamond, color: Colors.blue, size: 20),
Text(' $_watergirlGems/$_totalWatergirlGems'),
],
),
IconButton(
icon: Icon(_isPaused ? Icons.play_arrow : Icons.pause),
onPressed: () {
setState(() {
_isPaused = !_isPaused;
});
},
),
],
),
),
// 游戏区域
Expanded(
child: GestureDetector(
onTap: () {
setState(() {
_showControls = !_showControls;
});
},
child: Container(
color: Colors.grey[800],
child: Stack(
children: [
// 平台
..._platforms.map((platform) => Positioned(
left: platform.x.toDouble(),
top: platform.y.toDouble(),
child: Container(
width: platform.width.toDouble(),
height: platform.height.toDouble(),
decoration: BoxDecoration(
color: Colors.brown[700],
borderRadius: BorderRadius.circular(4),
border: Border.all(color: Colors.brown[900]!, width: 2),
),
),
)),
// 岩浆
..._lavaPools.map((lava) => Positioned(
left: lava.x.toDouble(),
top: lava.y.toDouble(),
child: Container(
width: lava.width.toDouble(),
height: lava.height.toDouble(),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.red, Colors.orange, Colors.yellow],
),
borderRadius: BorderRadius.circular(8),
),
),
)),
// 水池
..._waterPools.map((water) => Positioned(
left: water.x.toDouble(),
top: water.y.toDouble(),
child: Container(
width: water.width.toDouble(),
height: water.height.toDouble(),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue.shade300, Colors.blue, Colors.blue.shade900],
),
borderRadius: BorderRadius.circular(8),
),
),
)),
// 宝石
..._gems.map((gem) => Positioned(
left: gem.x.toDouble(),
top: gem.y.toDouble(),
child: Container(
width: 20,
height: 20,
decoration: BoxDecoration(
color: gem.color,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: gem.color.withOpacity(0.6),
blurRadius: 10,
spreadRadius: 2,
),
],
),
),
)),
// 门
Positioned(
left: _redDoorPosition.dx,
top: _redDoorPosition.dy,
child: Container(
width: 40,
height: 60,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.red.shade400, Colors.red.shade900],
),
borderRadius: BorderRadius.circular(4),
border: Border.all(color: Colors.yellow, width: 2),
),
child: const Icon(Icons.exit_to_app, color: Colors.yellow),
),
),
Positioned(
left: _blueDoorPosition.dx,
top: _blueDoorPosition.dy,
child: Container(
width: 40,
height: 60,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue.shade400, Colors.blue.shade900],
),
borderRadius: BorderRadius.circular(4),
border: Border.all(color: Colors.yellow, width: 2),
),
child: const Icon(Icons.exit_to_app, color: Colors.yellow),
),
),
// 火焰人
Positioned(
left: _fireboyPosition.dx,
top: _fireboyPosition.dy,
child: Container(
width: _playerSize.width,
height: _playerSize.height,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.orange, Colors.red],
),
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.orange.withOpacity(0.6),
blurRadius: 10,
spreadRadius: 2,
),
],
),
child: const Icon(Icons.local_fire_department, color: Colors.white),
),
),
// 冰人
Positioned(
left: _watergirlPosition.dx,
top: _watergirlPosition.dy,
child: Container(
width: _playerSize.width,
height: _playerSize.height,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.lightBlue, Colors.blue],
),
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.blue.withOpacity(0.6),
blurRadius: 10,
spreadRadius: 2,
),
],
),
child: const Icon(Icons.ac_unit, color: Colors.white),
),
),
// 暂停覆盖层
if (_isPaused)
Container(
color: Colors.black.withOpacity(0.7),
child: const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.pause, size: 80, color: Colors.white),
SizedBox(height: 16),
Text('暂停', style: TextStyle(fontSize: 32, color: Colors.white)),
],
),
),
),
// 游戏结束覆盖层
if (_gameOver)
Container(
color: Colors.black.withOpacity(0.8),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.close, size: 80, color: Colors.red),
const SizedBox(height: 16),
const Text('游戏结束', style: TextStyle(fontSize: 32, color: Colors.white)),
const SizedBox(height: 16),
Text(_deathReason, style: const TextStyle(fontSize: 18, color: Colors.white)),
const SizedBox(height: 32),
ElevatedButton(
onPressed: _restartLevel,
child: const Text('重新开始'),
),
],
),
),
),
// 胜利覆盖层
if (_gameWin)
Container(
color: Colors.black.withOpacity(0.8),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.emoji_events, size: 80, color: Colors.yellow),
const SizedBox(height: 16),
const Text('过关!', style: TextStyle(fontSize: 32, color: Colors.white)),
const SizedBox(height: 16),
Text(
'收集宝石: 火焰人 $_fireboyGems/$_totalFireboyGems, 冰人 $_watergirlGems/$_totalWatergirlGems',
style: const TextStyle(fontSize: 16, color: Colors.white),
),
const SizedBox(height: 32),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: _restartLevel,
child: const Text('重新开始'),
),
const SizedBox(width: 16),
ElevatedButton(
onPressed: _nextLevel,
child: const Text('下一关'),
),
],
),
],
),
),
),
// 虚拟控制
if (_showControls && !_gameOver && !_gameWin && !_isPaused)
Positioned(
bottom: 20,
left: 20,
right: 20,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// 冰人控制
Column(
children: [
GestureDetector(
onTapDown: (_) => setState(() => _watergirlJumpPressed = true),
onTapUp: (_) => setState(() => _watergirlJumpPressed = false),
onTapCancel: () => setState(() => _watergirlJumpPressed = false),
child: Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.7),
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
),
child: const Icon(Icons.arrow_upward, color: Colors.white),
),
),
const SizedBox(height: 10),
Row(
children: [
GestureDetector(
onTapDown: (_) => setState(() => _watergirlLeftPressed = true),
onTapUp: (_) => setState(() => _watergirlLeftPressed = false),
onTapCancel: () => setState(() => _watergirlLeftPressed = false),
child: Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.7),
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
),
child: const Icon(Icons.arrow_back, color: Colors.white),
),
),
const SizedBox(width: 10),
GestureDetector(
onTapDown: (_) => setState(() => _watergirlRightPressed = true),
onTapUp: (_) => setState(() => _watergirlRightPressed = false),
onTapCancel: () => setState(() => _watergirlRightPressed = false),
child: Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.7),
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
),
child: const Icon(Icons.arrow_forward, color: Colors.white),
),
),
],
),
const SizedBox(height: 8),
const Text('冰人', style: TextStyle(color: Colors.blue, fontWeight: FontWeight.bold)),
],
),
// 火焰人控制
Column(
children: [
GestureDetector(
onTapDown: (_) => setState(() => _fireboyJumpPressed = true),
onTapUp: (_) => setState(() => _fireboyJumpPressed = false),
onTapCancel: () => setState(() => _fireboyJumpPressed = false),
child: Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: Colors.orange.withOpacity(0.7),
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
),
child: const Icon(Icons.arrow_upward, color: Colors.white),
),
),
const SizedBox(height: 10),
Row(
children: [
GestureDetector(
onTapDown: (_) => setState(() => _leftPressed = true),
onTapUp: (_) => setState(() => _leftPressed = false),
onTapCancel: () => setState(() => _leftPressed = false),
child: Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: Colors.orange.withOpacity(0.7),
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
),
child: const Icon(Icons.arrow_back, color: Colors.white),
),
),
const SizedBox(width: 10),
GestureDetector(
onTapDown: (_) => setState(() => _rightPressed = true),
onTapUp: (_) => setState(() => _rightPressed = false),
onTapCancel: () => setState(() => _rightPressed = false),
child: Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: Colors.orange.withOpacity(0.7),
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
),
child: const Icon(Icons.arrow_forward, color: Colors.white),
),
),
],
),
const SizedBox(height: 8),
const Text('火焰人', style: TextStyle(color: Colors.orange, fontWeight: FontWeight.bold)),
],
),
],
),
),
// 隐藏控制提示
if (!_showControls)
Positioned(
top: 80,
left: 0,
right: 0,
child: Center(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.6),
borderRadius: BorderRadius.circular(20),
),
child: const Text('点击屏幕显示/隐藏控制', style: TextStyle(color: Colors.white)),
),
),
),
],
),
),
),
),
// 键盘操作说明
Container(
padding: const EdgeInsets.all(12),
color: Colors.black87,
child: Wrap(
alignment: WrapAlignment.center,
spacing: 16,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
const Text('火焰人: ', style: TextStyle(color: Colors.orange)),
_buildKeyBadge('A'),
_buildKeyBadge('D'),
_buildKeyBadge('W'),
],
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
const Text('冰人: ', style: TextStyle(color: Colors.blue)),
_buildKeyBadge('J'),
_buildKeyBadge('L'),
_buildKeyBadge('I'),
],
),
],
),
),
],
),
);
}
Widget _buildKeyBadge(String key) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 2),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: Colors.grey[700],
borderRadius: BorderRadius.circular(6),
border: Border.all(color: Colors.grey[500]!),
),
child: Text(key, style: const TextStyle(fontSize: 14)),
);
}
}
// 游戏元素类
class Platform {
final int x;
final int y;
final int width;
final int height;
const Platform({
required this.x,
required this.y,
required this.width,
required this.height,
});
}
class LavaPool {
final int x;
final int y;
final int width;
final int height;
const LavaPool({
required this.x,
required this.y,
required this.width,
required this.height,
});
}
class WaterPool {
final int x;
final int y;
final int width;
final int height;
const WaterPool({
required this.x,
required this.y,
required this.width,
required this.height,
});
}
class Gem {
final double x;
final double y;
final Color color;
const Gem({
required this.x,
required this.y,
required this.color,
});
}
class Lever {
final double x;
final double y;
bool isActive;
Lever({
required this.x,
required this.y,
this.isActive = false,
});
}
class Gate {
final double x;
final double y;
bool isOpen;
Gate({
required this.x,
required this.y,
this.isOpen = false,
});
}