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

前言

这篇文章是接着上一章写的,若是没有看过上一章,能够经过查看公众号"bugporter"的历史记录获取上一章的内容,或者经过如下连接查看。bash

juejin.im/post/5ea1a5…框架

优化上一章的代码

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

以前的地面(Horizon)组件类示例:函数

lib/sprite/horizon.dartoop

class Horizon ...{
  ...
  Horizon(this.spriteImage);
  
  @override
  void resize(ui.Size size) {
    super.resize(size);
    if(components.isEmpty){
      init();
      return;
    }
  }
  ...
}
复制代码

更改后post

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)ui

除了跳小恐龙还会什么?this

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

  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: ... 是省略以前的代码的意思

打包运行:

恐龙飞起来了,是由于在添加时,还没给它设置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看着有点卡,其实是很流畅的..

下一章继续完善...

相关文章
相关标签/搜索