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
點個關注吧~