这篇文章是接着上一章写的,若是没有看过上一章,能够经过查看公众号"bugporter"的历史记录获取上一章的内容,或者经过如下连接查看。bash
上一章全部须要用到屏幕尺寸的 组件(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
这里面有两个状态我解释一下:
等待: 游戏未开始时小恐龙的样子,开始后它须要跑到屏幕的必定距离,咱们才能控制它
惊讶: 这图像中的小恐龙很惊讶,由于它碰到障碍物,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轴的位置,屏幕高-(地面高+恐龙高-再站下一点点的距离)
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, 运行后..