FLAME引擎
这里我用了一款基于的Flutter的游戏引擎,名为FLAME
。使用它开发游戏,基本用不到Flutter里的那些Widget
,所以就有了标题里的提问
一个Flutter应用不使用Flutter的Widget
,那还叫Flutter吗?
不管了,先把这篇文章写完,抽空再用widget
重做一次这个游戏
游戏引擎基础
引入Flame
库
dependencies: flame: ^1.0.0-rc5
开发游戏,最基础就是这三件事
- 刷新机制(update)
- 渲染画布(renderer)
- 输入事件(events)
在Flame
中,一个基础的游戏框架代码如下
class MyGameSubClass extends Game { @override void render(Canvas canvas) { // TODO: implement render } @override void update(double t) { // TODO: implement update } } main() { runApp( GameWidget( game: MyGameSubClass(), ) ); }
刷新,渲染都有了,事件呢,在本游戏中我们加入拖动事件
class MyGameSubClass extends BaseGame with PanDetector
渲染图形元素
- 飞机
飞机的图形素材是一个精灵图,其中前面两帧是飞行状态,后面三帧是爆炸状态
playerSpriteSheet = SpriteSheet( image: await images.load('player.png'), srcSize: playerSize, ); //运行状态下的精灵图动画 player = SpriteAnimationComponent( size: playerSize, animation: playerSpriteSheet.createAnimation(row: 0, stepTime: .1, to: 2), );
然后我们通过add
方法,将图形元素展示到画面中
add(player); //放到x:100,y:100的坐标,大家感受一下坐标系 player.x=player.y=100;
- 子弹
bulletImage = await images.load('bullet.png');
一颗子弹的出现逻辑以及生命周期是这样的
- 从屏幕外生成
- 向着飞机当前坐标的方向移动
- 移出屏幕即销毁
//添加一颗子弹 void addBullet() { final bullet = SpriteComponent.fromImage(bulletImage, size: bulletSize); double bulletX; double bulletY; //随机在显示区四周的矩形边上生成 if (Random().nextBool()) { bulletX = Random().nextDouble() * (screenSize.width + bulletSize.x) - bulletSize.x; bulletY = Random().nextBool() ? -bulletSize.y : screenSize.height; } else { bulletX = Random().nextBool() ? -bulletSize.x : screenSize.width; bulletY = Random().nextDouble() * (screenSize.height + bulletSize.y) - bulletSize.y; } //给与子弹初始坐标 bullet.x = bulletX; bullet.y = bulletY; //添加到场景中 add(bullet); //加入bullet管理数组,稍后用于更新子弹飞行坐标以及碰撞检测 bullets.add({ "component": bullet,//子弹控件实例 "speed": (1+gameTime/10) + Random().nextDouble()*3,//飞行速度 "angle": atan2(((bulletY + bulletSize.y/2) - (player.y + playerSize.y / 2)), ((bulletX + bulletSize.x) - (player.x + playerSize.x / 2))) });//向量角度 }
通过调用这个addBullet
的方法,我们就能在屏幕中朝着飞机所在位置发射一颗子弹了
为了发射多颗,我们再新建一个函数addGroupBullet
//添加一组子弹 void addGroupBullet() { int groupCount = 10+Random().nextInt(gameTime+1); for (int i = 0; i < groupCount; i++) { addBullet(); } }
刷新机制
在Flame
提供的update
方法中我们需要更新所有子弹的位置,因为子弹不是静止的,需要一直移动呀!
@override void update(double dt) { super.update(dt); //遍历子弹数组,对所有子弹进行更新 for (int i = bullets.length - 1; i >= 0; i--) { var bulletItem = bullets[i]; //获得子弹实例 SpriteComponent bullet = bulletItem["component"] as SpriteComponent; double angle = bulletItem["angle"]; double speed = bulletItem["speed"]; //让子弹根据发射时的向量角度进行移动 bullet.x -= cos(angle) * speed; bullet.y -= sin(angle) * speed; //当子弹移动到屏幕外时销毁它 if (isNotInScreen(bullet.x, bullet.y)) { print("bullet removed"); remove(bullet); bullets.removeAt(i); continue; } } }
开始有点带感
了吧?
飞机的移动
void onPanUpdate(DragUpdateDetails details) { super.onPanUpdate(details); if (!isGameStart) return; //由于飞机对象的坐标点在左上角,所以移动是要注意偏移一下 player.x = details.globalPosition.dx - playerSize.x / 2; player.y = details.globalPosition.dy - playerSize.y / 2; }
碰撞检测
在每次刷新子弹坐标的时候,去检测一下是否和当前飞机的坐标重合了,重合了游戏就结束啦
if (isHitPlayer(bullet.x, bullet.y)) { gameOver(); }
这里用了一个简单的判断机制,并没有精确到飞机的外轮廓。
一是精确到外轮廓将带来更大的运算量,也比较复杂。
二是那样的游戏体验不见得好,毕竟在手机玩的话,手指要挡住大部分飞机的图形。
//飞机和子弹碰撞判断 bool isHitPlayer(double x, double y) { double _x = (x + bulletSize.x / 2 - (player.x + playerSize.x / 2)).abs(); double _y = (y + bulletSize.y / 2 - (player.y + playerSize.y / 2)).abs(); //求出子弹和飞机坐标的直线距离 double distance = sqrt(_x * _x + _y * _y); //当直线距离小于判定为碰撞的距离时则返回true/false if (distance <= hitDistance) return true; return false; }
游戏结束
void gameOver() { isGameStart = false; //取消定时发射子弹的定时器 if(timer!=null)timer.cancel(); //播放飞机爆炸动画 player.animation = playerSpriteSheet.createAnimation( row: 0, stepTime: .1, loop: false, from: 2, to: 6); print("game over"); }
后话
这么写,从代码层面看,和Flutter关系不大,但利用Flutter的跨端能力,我们可以很容易的开发一个横跨Mac,Linux,Windows,Web,iOS,Android端的小游戏。如果你不会用那些开发游戏的IDE,比如Unity3D等,那Flutter也是一个不错的选择。