FPS全称是“Frames Per Second”,翻译为“每秒传输帧数”。在代码中一般会定义一个循环来表示,这个循环由两部分组成,分别是:更新(update)和渲染(render)。git
渲染(rende)部分只负责一件事,在更新(update)部分发生变化时,绘制屏幕上的全部对象。chrome
在这个循环中, 每次循环就是游戏中的一帧,每次循环消耗的时间越短,帧数就越高。canvas
Flutter中有一个插件叫Flame,这个插件提供了一个完整的游戏开发框架,底层中实现了循环机制,使用它咱们只须要编写游戏更新和渲染的代码。浏览器
在谷歌浏览器输入chrome://dino,打开网页调试工具,会发现整个游戏只有一张图片。bash
打开项目中的pubspec.yaml文件,配置好插件和图片 框架
坐标x和y轴都是从左上角开始的dom
flame插件提供了一个util类,提供了一些实用的功能,例如获取屏幕尺寸、设置屏幕方向等,能够直接用它快速实现横屏全屏显示async
打开main.dart文件,在main方法中输入如下代码ide
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Flame.util
..fullScreen()
..setLandscape();
}
复制代码
因为fullScreen和setLandscape方法须要等flutter框架和widgets组件绑定后才能调用,因此须要WidgetsFlutterBinding.ensureInitialized来确保已经绑定,否则会报错工具
flame提供了两个抽象类,对游戏循环概念进行了简单的抽象,它们分别是Game和BaseGame。
它们都定义了update和rende方法:
大多数游戏都是基于这两个方法实现的
BaseGame继承了Game,BaseGame实现了以组件(component)为基础的game。它提供了一个组件列表,每一个组件都表示游戏中的一个或多个对象,它们能够是地图、人物、动画等。在BaseGame的rende方法中,会把每一个组件都渲染出来。
本文中使用的是BaseGame
在lib目录,添加一个game.dart文件,在里面建立MyGame类,让它继承BaseGame。
import 'dart:ui' as ui;
import 'package:flame/game.dart';
class MyGame extends BaseGame{
MyGame(ui.Image spriteImage) {}
@override
void update(double t) {}
@override
void render(Canvas canvas) {}
}
复制代码
这个游戏只有一张图片,因此在构造方法中接收了一个图片实例。
回到main.dart,把编写的游戏类显示出来,main.dart完整代码:
import 'dart:ui' as ui;
import 'package:flame/flame.dart';
import 'package:flutter/material.dart';
import 'package:fluttergame/game.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
Flame.util
..fullScreen()
..setLandscape();
ui.Image image = await Flame.images.load("sprite.png");
runApp(MyGame(image).widget);
}
复制代码
编译运行后,打开游戏是黑屏的,由于还没在游戏中编写任何东西。
咱们知道,BaseGame提供了一个组件列表,咱们能够把游戏中每一个对象都封装成一个组件,而后把它添加进游戏中。
打开game.dart,在MyGame类的下面添加一个组件类GameBg(继承Component)
...
class MyGame...
class GameBg extends Component with Resizable {
Color bgColor;
GameBg([this.bgColor = Colors.white]);
@override
void render(Canvas canvas) {
Rect bgRect = Rect.fromLTWH(0, 0, size.width, size.height);
Paint bgPaint = Paint();
bgPaint.color = bgColor;
canvas.drawRect(bgRect, bgPaint);
}
@override
void update(double t) {}
}
复制代码
由于整个游戏背景都是一种颜色的,因此在上面代码的render方法中,在canvas上画了一个宽高都等于屏幕大小的矩形(Rect),屏幕的宽高是经过size这个属性获取的。
而size这个属性是在Resizable这个类中定义的,Resizable是with进来的,里面覆盖了Component的resize方法。resize方法接收了一个size参数,每次resize方法被调用的时候都会把size属性更新了。(关于组件的resize方法在何时被调用,下文会说到。)
背景组件建立好了,须要在MyGame中使用它,先给MyGame添加一个GameBg属性,方便后面更改背景颜色
...
class MyGame extends BaseGame{
GameBg gameBg;
...
复制代码
而后在构造方法中,实例化背景组件,把它添加到组件列表中
...
MyGame(ui.Image spriteImage) {
gameBg = GameBg(Colors.white);
this.add(gameBg);
}
...
复制代码
组件是经过BaseGame类的add方法添加进去的,添加进了components这个属性中,components是一个有序集合。
如今从新运行一下,会发现屏幕已经从黑色变成白色了,咱们的背景组件已经生效了。它为何会生效呢?
上文中介绍了Game类的update和render方法,除了这两个,Game还有一个resize方法,它是在第一次循环和后面屏幕尺寸被改变的时候才会被调用,接收了一个Size参数,里面包含了屏幕的宽和高。
组件(Component)是一个抽象类,类中定义resize、update和render这3个方法,它是被BaseGame类调用的。
BaseGame类继承了Game,它重写了Game的resize、update和render这3个方法。 在这3个方法中,都遍历了组件列表,在遍历中调用了组件的同名方法,把当前接收到的参数传了进去。
在游戏渲染的时候,会带来一个问题,组件的render方法接收的都是Game类的Canvas,是在Game类的Canvas中绘制内容的,因此BaseGame后面添加的组件会在前一个组件的上面。
咱们在开发的时候,就须要先肯定好组件的层次,例如,上面的背景放到了第一层。
在MyGame类中,构造方法接收了一个图片实例,这是一张精灵表,里面包含了地面图片,因此咱们须要把它显示出来。
Sprite.fromImage(
Image image, {
double x,
double y,
double width,
double height,
})
复制代码
在精灵表中每一个图像精灵的坐标和宽高都须要先测量出来,它们是不变的,因此为他们写一个配置类。
在lib目录建立一个config.dart文件,建立HorizonConfig类,它是地面的配置,里面包含地面在精灵表中的坐标和宽高
class HorizonConfig{
static double w = 2400/3;
static double h = 38.0;
static double y = 104.0;
static double x = 2.0;
}
复制代码
上面代码中地面在精灵表中的宽度是2400的,为何分红3份呢?
由于整个游戏的地面宽度是无限的,可是图片宽度是有限的,要实现无限地面通常都是加载两个地面,地面的x坐标不断减小,直到一个地面超出屏幕外面后,再把这个地面设置到另外一个地面的后面。
这种作法的地面都是重复的,因此把这种作法pass掉了。个人作法是将地面分红了3份,每次循环的时候,最左边的地面超出了屏幕后就删掉它,而后在最后一个地面的后面随机建立整个地面中的某一份。
如今来建立地面组件
在lib中建立一个sprite目录,后面建立的组件都放在这里。
而后在这个目录中建立一个horizon.dart,在里面写咱们的地面组件Horizon类
...
class Horizon extends PositionComponent
with HasGameRef, Tapable, ComposedComponent, Resizable {
final ui.Image spriteImage;
Horizon(this.spriteImage);
}
复制代码
上面代码中继承的是PositionComponent,PositionComponent继承了Component,添加了一些功能,例如设置组件在游戏中的坐标、组件的宽度等。
with了ComposedComponent类,这个类给PositionComponent提供了一个组件列表,和BaseGame同样,咱们能够添加其它组件到这里,也就是说咱们能够嵌套组件。
为Horizon类写一个建立随机地面的方法
...
SpriteComponent createComposer(double x) {
final Sprite sprite = Sprite.fromImage(spriteImage,
width: HorizonConfig.w,
height: HorizonConfig.h,
y: HorizonConfig.y,
x: HorizonConfig.w * (Random().nextInt(3)) + HorizonConfig.x
);
SpriteComponent horizon = SpriteComponent.fromSprite(
HorizonConfig.w, HorizonConfig.h, sprite);
horizon.y = size.height - HorizonConfig.h;
horizon.x = x;
return horizon;
}
...
复制代码
这个方法中用到了SpriteComponent,SpriteComponent继承了PositionComponent,提供渲染精灵的功能。
“horizon.y = size.height - HorizonConfig.h” 这个代码把地面设置在屏幕底部,用到了size这个属性,因此须要resize方法被第一次调用后,咱们才能使用上面的方法
初始化地面
...
SpriteComponent lastComponent;
...
@override
void resize(ui.Size size) {
super.resize(size);
if(components.isEmpty){
init();
return;
}
}
void init(){
double x = 0;
int count = (size.width/HorizonConfig.w).ceil() + 1;
for(int i=0; i<count; i++){
lastComponent = createComposer(x);
x += HorizonConfig.w;
add(lastComponent);
}
}
...
复制代码
在init方法中,根据屏幕宽度肯定要建立多少个地面。
而后,让地面都开始动起来
...
@override
void update(double t) {
double x = t * 50 * 6.5;
for(final c in components){
final component = c as SpriteComponent;
//释放前面超出屏幕的地面, 再从新添加一个在后面
if(component.x + HorizonConfig.w < 0){
components.remove(component);
SpriteComponent horizon = createComposer(lastComponent.x + HorizonConfig.w);
add(horizon);
lastComponent = horizon;
continue;
}
component.x -= x;
}
}
...
复制代码
t*50是逻辑上的速度, 6.5是速率,像这种跑酷游戏通常都是给它定一个逻辑上的速度,再把速率调到满意为止
咱们须要获取最后一个组件的x坐标,可是在集合中,要获取指定的元素都是经过再次遍历获取的。因此为了减小遍历,设置了一个lastComponent属性,保存了最后一个组件。
定位到MyGame这个类中,添加地面组件
...
GameBg gameBg;
Horizon horizon;
MyGame(ui.Image spriteImage) {
gameBg = GameBg(Color.fromRGBO(245, 243, 245, 1));
horizon = Horizon(spriteImage);
this
..add(gameBg)..add(horizon);
}
...
复制代码
运行...
打开sprite.png,测量云朵的位置和宽高,而后在config.dart中建立CloudConfig类写上。
太透明了,看了几遍才找到~
...
class CloudConfig{
static double w = 92.0;
static double h = 28.0;
static double y = 2.0;
static double x = 166.0;
}
复制代码
封装一个获取指定范围的随机数方法
在lib目录中,建立util.dart,写上如下代码
import 'dart:math';
double getRandomNum(double min, double max) =>
(Random().nextDouble() * (max - min + 1)).floor() + min;
复制代码
建立云朵的时候会用到
云朵组件
在lib/sprite目录中添加一个cloud.dart文件,在里面建立一个Cloud类
class Cloud extends PositionComponent
with HasGameRef, Tapable, ComposedComponent, Resizable {
final ui.Image spriteImage;
SpriteComponent lastComponent;
double maxY = 0;
double minY = 5;
Cloud(this.spriteImage);
SpriteComponent createComposer(double x, double y) {
final Sprite sprite = Sprite.fromImage(spriteImage,
width: CloudConfig.w,
height: CloudConfig.h,
y: CloudConfig.y,
x: CloudConfig.x);
SpriteComponent component =
SpriteComponent.fromSprite(CloudConfig.w, CloudConfig.h, sprite);
component.x = x;
component.y = y;
return component;
}
}
复制代码
和地面组件同样,添加了一个createComposer方法建立云朵, 云朵的x和y都是随机的,可是要控制一下随机范围,否则云朵会覆盖以前的。云朵还要在地面的上面,因此定义两个参数,控制一下y的位置:maxY、minY。
maxY要根据地面的y轴来判断,因此要在size属性被加载的时候才能定义。
初始化云朵
...
@override
void resize(ui.Size size) {
super.resize(size);
maxY = size.height - CloudConfig.h - HorizonConfig.h;
if (components.isEmpty) {
init();
return;
}
}
void init() {
int count = 6;
for (int i = 0; i < count; i++) {
double x, y;
y = getRandomNum(minY, maxY);
x = (lastComponent != null ? lastComponent.x + CloudConfig.w : 0) +
getRandomNum(1, size.width / 2);
lastComponent = createComposer(x, y);
add(lastComponent);
}
}
...
复制代码
init方法中,直接添加随机位置的云朵有可能会覆盖,这里简单的处理了一下,添加的云朵x轴大于上一个的。
而后让云朵开始飘
...
@override
void update(double t) {
double x = t * 8 * 6.5;
for (final c in components) {
final component = c as SpriteComponent;
if (component.x + CloudConfig.w < 0) {
double lastX = lastComponent.x + CloudConfig.w;
if (size.width > lastX) lastX = size.width;
component.x = lastX + getRandomNum(1, size.width / 2);
component.y = getRandomNum(minY, maxY);
lastComponent = component;
continue;
}
component.x -= x;
}
}
...
复制代码
云朵的速度设置得比地面的速度慢,视差效果,最远的老是比近的慢。
这里不像地面组件同样,再从新建立,而是把超出屏幕左边的云朵坐标从新设置。坐标的x轴也简单处理了一下,让它不会覆盖到其它云朵上面。
更改MyGame类,把云朵组件加上
class MyGame extends BaseGame with TapDetector {
GameBg gameBg;
Horizon horizon;
Cloud cloud;
MyGame(ui.Image spriteImage) {
gameBg = GameBg(Color.fromRGBO(245, 243, 245, 1));
horizon = Horizon(spriteImage);
cloud = Cloud(spriteImage);
this
..add(gameBg)..add(horizon)..add(cloud);
}
}
复制代码
运行
这一篇就到这了,下一篇再将这个游戏完善~
公众号:bugporter
点个关注吧~