vlambda博客
学习文章列表

flutter开发游戏入门(仿谷歌浏览器小恐龙Chrome dino)二

前言

https://juejin.im/post/5ea1a584e51d45470a4ad4eb

优化上一章的代码

上一章所有需要用到屏幕尺寸的 组件(Component)类都是在resize方法中接收到包含屏幕尺寸的Size参数后才构建的。但是每个类都这样写,有点不友好,所以我把构造方法改了一下,让它直接接收Size参数,然后在MyGame类的resize方法中,把接收到Size参数给到组件后再实例化这些组件。

之前的地面(Horizon)组件类示例:

lib/sprite/horizon.dart

class Horizon ...{
...
Horizon(this.spriteImage);

@override
void resize(ui.Size size) {
super.resize(size);
if(components.isEmpty){
init();
return;
}
}
...
}

更改后

class Horizon ...{
...
ui.Size size;

Horizon(this.spriteImage, this.size){
init();
}
//不再需要重写resize了

其它组件类也这样改,然后我们在MyGame类的resize中,才实例化这些组件

lib/game.dart

Class MyGame...
@override
void resize(ui.Size size) {
if(components.isEmpty){
gameBg = GameBg(Color.fromRGBO(245, 243, 245, 1));
horizon = Horizon(spriteImage, size);
cloud = Cloud(spriteImage, size);
obstacle = Obstacle(spriteImage, size);
}
super.resize(size);
}
...

创建小恐龙

在上一章已经完成了游戏背景、地面、和天空(云朵),现在来创建游戏最重要的一部分,游戏主角,那个会跳不会rap也不会篮球的 小恐龙(dino)。

flutter开发游戏入门(仿谷歌浏览器小恐龙Chrome dino)二

除了跳小恐龙还会什么?

flutter开发游戏入门(仿谷歌浏览器小恐龙Chrome dino)二

这里面有两个状态我解释一下:

  1. 等待: 游戏未开始时小恐龙的样子,开始后它需要跑到屏幕的一定距离,我们才能控制它

  2. 惊讶: 这图像中的小恐龙很惊讶,因为它碰到障碍物,Game Over了!

知道这些状态后,需要测量出这些状态对应的图像位置和大小,然后把它写到配置中。

lib/config.dart

...
class DinoConfig{
static double h = 94.0;
static double y = 2.0;
}
class DinoJumpConfig{
static double w = 88.0;
static double x = 1336.5;
}
class DinoWaitConfig{
static double w = 88.0;
static double x = 1336.5+88;
}
class DinoRunConfig{
static double w = 88;
final double x;

const DinoRunConfig._internal({this.x});

static List<DinoRunConfig> list = [
DinoRunConfig._internal(
x: 1336.5+(88*2)
),
DinoRunConfig._internal(
x: 1336.5+(88*3)
),
];
}
class DinoDieConfig{
static double w = 88;
static double x = 1336.5+(88*4);
}
class DinoDownConfig{
static double w = 118;
final double x;

const DinoDownConfig._internal({this.x});

static List<DinoDownConfig> list = [
DinoDownConfig._internal(
x: 1866.0
),
DinoDownConfig._internal(
x: 1866.0+118
),
];
}

上面代码中,我为小恐龙每个状态的图像位置都创建了一个配置类。在这些配置中,它们的h(高)和y轴有些不是一样的,所以我把它放到DinoConfig中,把这些状态的高和y轴都强制一样,可以方便控制它的y轴实现跳跃。不然的话,需要计算每个状态的跳跃高度,还有站在地面上的高度。

里面的蹲和站两个跑步状态是由多个图像组成的动画,所以我为它们写了一个私有的构造方法,并通过一个静态的List返回每个图像不同的地方。

为什么要这样返回呢?是因为在flame这个框架中,它为我们提供了一个动画Animation类来创建动画,我们可以通过它的spriteList构造方法来创建。在这个方法中,需要一个Sprite类型的List,所以我们可以通过遍历配置中的List,把创建的Sprite对象加入到动画组件的List中。

栗子

List<Sprite> runSpriteList = [];
DinoRunConfig.list.forEach((DinoRunConfig config){
runSpriteList.add(Sprite.fromImage(spriteImage,
x: config.x,
y: DinoConfig.y,
width: DinoRunConfig.w,
height: DinoConfig.h),
);
});
//AnimationComponent 动画组件,需要3个参数,宽、高和动画对象。
//stepTime每帧的时间,loop是否循环播放
AnimationComponent(
DinoRunConfig.w,
DinoConfig.h,
Animation.spriteList(runSpriteList, stepTime: 0.1, loop: true));

这里面有个地方需要注意一下,如果在父组件中把这个动画组件添加进去了,但是重写了父的update方法时,还需要在父的update中调用动画组件的update方法,这个动画才会播放。

配置写好了,现在来创建主角的组件。打开lib/script目录,在这个目录下创建一个dino.dart

在dino.dart中,先创建一个枚举,把小恐龙在整个游戏中的状态写上

enum DinoStatus {
waiting,
running,
jumping,
downing,
die,
}

五个状态,分别是:等待中、跑步中、跳跃中、正在蹲着和game over了

创建好了之后,在枚举代码的下边,我们创建一个组件类dino。在这个类中定义一个list属性,并把上面枚举对应状态的组件都添加进去,最后还需要一个status属性来记录小恐龙当前的状态。

enum DinoStatus...
class Dino extends Component{
List<PositionComponent> actualDinoList = List(5);
DinoStatus status = DinoStatus.waiting; //默认是等待中

Dino(ui.Image spriteImage, this.size) {
final double height = DinoConfig.h;
final double yPos = DinoConfig.y;

//创建枚举对应的组件,加进list属性
//waiting
actualDinoList[0] = SpriteComponent.fromSprite(
DinoWaitConfig.w,
height,
Sprite.fromImage(spriteImage,
x: DinoWaitConfig.x,
y: yPos,
width: DinoWaitConfig.w,
height: height));

//running
List<Sprite> runSpriteList = [];
DinoRunConfig.list.forEach((DinoRunConfig config){
runSpriteList.add(Sprite.fromImage(spriteImage,
x: config.x,
y: yPos,
width: DinoRunConfig.w,
height: height),
);
});
actualDinoList[1] = AnimationComponent(
DinoRunConfig.w,
height,
Animation.spriteList(runSpriteList,
stepTime: 0.1,
loop: true));


//jumping
actualDinoList[2] = SpriteComponent.fromSprite(
DinoJumpConfig.w,
height,
Sprite.fromImage(spriteImage,
x: DinoJumpConfig.x,
y: yPos,
width: DinoJumpConfig.w,
height: height));

//downing
List<Sprite> downSpriteList = [];
DinoDownConfig.list.forEach((DinoDownConfig config){
downSpriteList.add(Sprite.fromImage(spriteImage,
x: config.x,
y: yPos,
width: DinoDownConfig.w,
height: height),
);
});
actualDinoList[3] = AnimationComponent(
DinoDownConfig.w,
height,
Animation.spriteList(downSpriteList,
stepTime: 0.1,
loop: true));

//die
actualDinoList[4] = SpriteComponent.fromSprite(
DinoDieConfig.w,
height,
Sprite.fromImage(spriteImage,
x: DinoDieConfig.x,
y: yPos,
width: DinoDieConfig.w,
height: height));
}
}

状态对应的组件加到list了,我们还需要根据当前的状态来渲染不同的组件。

首先在类中定义一个获取器,返回当前的状态对应的组件

Dino(ui.Image spriteImage, this.size)...

//获取当前状态对应的组件
PositionComponent get actualDino => actualDinoList[status.index];

然后重写render方法,把当前状态的组件渲染出来

...
@override
void render(ui.Canvas c) {
actualDino.render(c);
}
...

现在,小恐龙组件已经被创建好了,我们回到MyGame这个类中,把它添加进去

class MyGame...
...
Dino dino;

@override
void resize(ui.Size size) {
if(components.isEmpty){
...
dino = Dino(spriteImage, size);
this
..add(gameBg)..add(horizon)..add(cloud)..add(dino)
...

ps:...  是省略之前的代码的意思

打包运行:

flutter开发游戏入门(仿谷歌浏览器小恐龙Chrome dino)二恐龙飞起来了,是因为在添加时,还没给它设置y轴的位置,所以默认是0的。

现在我们给它添加一个y轴的位置,屏幕高-(地面高+恐龙高-再站下一点点的距离)

class Dino...
...
double maxY;
double x,y;

Dino(ui.Image spriteImage, this.size) {
final double height = DinoConfig.h;
final double yPos = DinoConfig.y;
maxY = size.height - (HorizonConfig.h + height - 22);
x = 0;
y = maxY;

//waiting
actualDinoList[0] = SpriteComponent.fromSprite(
DinoWaitConfig.w,
height,
Sprite.fromImage(spriteImage,
x: DinoWaitConfig.x,
y: yPos,
width: DinoWaitConfig.w,
height: height))
..x=x..y=y;

... 其他组件也这样设置一下x和y。
}

上面代码的maxY: 地面的位置,也就是恐龙最大的y轴位置。

dino类不需要添加子组件,因为它每次都是根据状态来渲染一个组件的,只是起到了调度的作用,所以没有继承PositionComponent,而是继承了基础的Component类。这样做的话,需要给它一个x和y属性,我们在渲染子组件的时候,把子组件的x和y设置成dino类的,可以方便外面控制或者获取,后面进行破撞检测的时候会用到。

现在再运行:

给游戏添加跳和蹲的按钮

打开main.dart文件,调用runApp方法时,是获取了Game的widget属性作为参数给runApp方法的。既然Game类返回了widget,那么我们也可以把它放到flutter的其它组件中,例如给它套一个Stack, 把游戏返回的widget放在底下,把一些按钮添加到游戏的上面,然后通过按钮的点击事件,实现对游戏的控制。

但是想偷懒,不想写一堆flutter的widget怎么办?

在fleam0.18.0以上的版本,提供了一个HasWidgetsOverlay类,只要我们在Game类中with了这个类,就可以使用addWidgetOverlay方法,把一个widget添加到游戏的上面了,它底层就是使用Stack封装的。

打开game.dart文件,给MyGame类添加一个创建按钮的方法

...
class MyGame...
Widget createButton({@required IconData icon, double right=0, double
bottom=0,
ValueChanged<bool>
onHighlightChanged}){
return Positioned(
right: right,
bottom: bottom,
child: MaterialButton(
onHighlightChanged: onHighlightChanged,
onPressed: (){},
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
child: Container(
width: 50,
height: 50,
decoration: new BoxDecoration(
color: Color.fromRGBO(0, 0, 0, 0.5),
//设置四周圆角 角度
borderRadius: BorderRadius.all(Radius.circular(50)),
//设置四周边框
border: new Border.all(width: 2, color: Colors.black),
),
child: Icon(icon, color: Colors.black,),
),
),
);
}
...

该方法接收一个按钮长按事件的回调函数onHighlightChanged,要想按钮监听长按事件,必须要给按钮一个点击事件onPressed,所以我在按钮的onPressed中写了一个空的回调函数。

为什么不直接用点击事件呢?

因为点击事件是在手指离开屏幕之后才触发的,会有一点延迟,所以用长按事件,可以监听到玩家按下和松开,在这里我需要它按下后就马上跳,还有蹲下需要一直按住按钮。

onHighlightChanged每次点击都会触发两次,在按下和松开按钮的时候触发,回调中接收了一个bool类型的参数,按下是true、松开是false

然后我们在MyGame的resize方法中,创建跳和蹲的按钮,然后调用addWidgetOverlay添加到游戏的上面

void resize(ui.Size size) {
...
this
..add(gameBg)..add(horizon)..add(cloud)..add(dino)..add(obstacle)
..addWidgetOverlay('upButton', createButton(
icon: Icons.arrow_drop_up,
right: 50,
bottom: 120,
onHighlightChanged: (isOn)=>dino?.jump(isOn),
))
..addWidgetOverlay('downButton', createButton(
icon: Icons.arrow_drop_down,
right: 50,
bottom: 50,
onHighlightChanged: (isOn)=>dino?.down(isOn),
));
...

在onHighlightChanged中调用dino类的jump和down方法,这两个方法还没有,我们需要在dino类中实现它。

class Dino...
...
bool isJump = false;
bool isDown = false;
double jumpVelocity = 0.0;
...
void jump(bool isOn) {
if(status == DinoStatus.running && isOn){
status = DinoStatus.jumping;
this.jumpVelocity = jumpPos;
isJump = true;
return;
}
isJump = false;
}

void down(bool isOn){
isDown = isOn;
if(status == DinoStatus.running && isOn){
status = DinoStatus.downing;
return;
}
if(status == DinoStatus.downing && !isOn){
status = DinoStatus.running;
return;
}
}

@override
void update(double t) {
if (status == DinoStatus.jumping) {
y += jumpVelocity;
jumpVelocity += gravity;
if(y > maxY){
status = DinoStatus.running;
y = maxY;
//一直按住,不断跳
jump(isJump);
//跳的过程中按了蹲,角色落地时蹲下
down(isDown);
}
}
actualDino..x=x..y=y;
actualDino.update(t);
}

跳跃的时候给了它一个瞬间向上的力,然后不断给它一个重力让它回到地面。只有跑的时候能跳或者蹲,如果是跳,回到地面后还按着跳没松开那么将继续跳,蹲的时候按下马上蹲,松开了就站着跑。

把默认状态改为runing, 运行后..

录成gif看着有点卡,实际上是很流畅的..

下一章继续完善...