一點(diǎn)點(diǎn)基礎(chǔ)
游戲主循環(huán)(GameLoop)
游戲主循環(huán)是游戲的核心,計(jì)算機(jī)一次又一次運(yùn)行的一組指令,用通俗的話來說,如果游戲有生命,那么游戲主循環(huán)就是游戲的心跳。
同時(shí)為了更好的理解游戲主循環(huán),還需要引入一個(gè)計(jì)算機(jī)圖像領(lǐng)域的知識(shí)——FPS,F(xiàn)PS全稱是“Frames Per Second”,翻譯為“每秒傳輸幀數(shù)”,意思就是,如果游戲以60FPS運(yùn)行,則計(jì)算機(jī)每秒運(yùn)行60次游戲主循環(huán)??偨Y(jié)一下就是,1幀==游戲主循環(huán)的一次運(yùn)行。
通常來說,游戲主循環(huán)由兩部分組成——更新(update)和渲染(render)。

如上圖,更新(update)部分負(fù)責(zé)處理對(duì)象的移動(dòng),這里的對(duì)象可以是主角、NPC、敵人、障礙物、地圖和其他需要更新的參數(shù)。你在游戲里能看到的大部分動(dòng)作都在這部分發(fā)生,比如,計(jì)算主角的98K射出的子彈是否接觸到敵人。
而渲染(render)部分通常只負(fù)責(zé)一件事,在更新(update)部分發(fā)生變化時(shí),繪制屏幕上的所有對(duì)象,以便玩家看到的一切都是同步的。
游戲同步機(jī)制
在游戲中,同步機(jī)制是非常重要的,可以想象一下,現(xiàn)在更新一個(gè)NPC的位置,NPC處于正常狀態(tài),所以,你讓NPC開始移動(dòng)。但是,此時(shí)有一個(gè)子彈距離NPC只有幾個(gè)像素的距離,你更新了子彈,它會(huì)擊中NPC。
現(xiàn)在NPC已經(jīng)死了,所以你不用繪制子彈。這個(gè)時(shí)候,你應(yīng)該繪制NPC倒地動(dòng)畫的第一幀。
然后,在下一個(gè)游戲主循環(huán)中,您將跳過更新NPC位置,因?yàn)镹PC已經(jīng)死了,所以您改為渲染NPC垂死動(dòng)畫的第一幀,而不是倒地動(dòng)畫第二幀。
這會(huì)給玩家?guī)硪环N游戲不穩(wěn)定的感覺,玩家在玩射擊游戲,射擊一個(gè)NPC的時(shí)候,NPC不會(huì)倒地,玩家再次射擊,但是在子彈擊中NPC之前,NPC就死了。
非同步渲染的不穩(wěn)定性能可能不易被察覺,特別是當(dāng)每秒運(yùn)行60幀的高幀頻率下,但如果這種情況經(jīng)常發(fā)生,玩家還是會(huì)感覺出來的,然后就罵辣雞游戲了。
所以,最好提前計(jì)算好所有內(nèi)容,并且當(dāng)計(jì)算完成后最終確定所有對(duì)象的狀態(tài)時(shí),再開始繪制屏幕。
開始擼碼
使用Flame插件
在pubspec.yaml下添加flame插件,并通過flutter packages get命令下載插件,或者使用Visual Studio Code保存文件會(huì)自動(dòng)下載插件。
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^0.1.2
flame: ^0.13.0
Flame插件已經(jīng)提供了一個(gè)完整的游戲開發(fā)框架,所以我們只需要專心編寫實(shí)際的更新和渲染過程。首先,需要將應(yīng)用程序轉(zhuǎn)化為游戲模式,要做兩個(gè)操作:全屏和縱向。而令人感到巴適的是,F(xiàn)lame插件已經(jīng)封裝好了這些實(shí)用的功能,我們只需要編寫調(diào)用代碼就可以了。
我們先在main.dart的頂部添加以下引用。
import 'package:flame/util.dart';
import 'package:flutter/services.dart';
然后在main.dart的main函數(shù)內(nèi)部創(chuàng)建Flame的Util類的實(shí)例,調(diào)用其實(shí)例的全屏(fullScreen)和設(shè)置方向(setOrientation)函數(shù),同時(shí)要注意,因?yàn)檫@些函數(shù)的返回值類型是未來(Future),所以要在這些函數(shù)前面添加等待(await)。
未來(Future)、異步(async)和等待(await)是一種特殊的編碼方法,它讓那些需要長時(shí)間才能處理完成的代碼在不同的線程上完成,而且不會(huì)阻塞主線程。
為了能夠等待(await)未來(Future)處理完成,相關(guān)的代碼必須在異步(async)函數(shù)內(nèi),所以我們必須修改main函數(shù),使它成為一個(gè)異步函數(shù)。
void main() async {
Util flameUtil = Util();
await flameUtil.fullScreen();
await flameUtil.setOrientation(DeviceOrientation.portraitUp);
到這里為止,我們的main.dart里面應(yīng)該有以下代碼。
import 'package:flutter/material.dart';
import 'package:flame/util.dart';
import 'package:flutter/services.dart';
void main() async {
Util flameUtil = Util();
await flameUtil.fullScreen();
await flameUtil.setOrientation(DeviceOrientation.portraitUp);
}
游戲主循環(huán)腳手架
在開頭,我們知道在一個(gè)游戲應(yīng)用中,游戲是在游戲主循環(huán)里面運(yùn)行的。Flame插件已經(jīng)提供了可以直接使用的游戲主循環(huán)腳手架,要使用這個(gè)腳手架,就要用到Flame的游戲(Game)抽象類。
創(chuàng)建一個(gè)名稱為box-game.dart的新文件,然后開始編寫BoxGame類,。
import 'dart:ui';
import 'package:flame/game.dart';
class BoxGame extends Game {
void render(Canvas canvas) {
// TODO: 實(shí)現(xiàn)渲染
}
void update(double t) {
// TODO: 實(shí)現(xiàn)更新
}
}
上面的代碼中,導(dǎo)入dart:ui庫,這樣的話,等一下我們就可以使用畫布(Canvas)類和大小(Size)類。然后導(dǎo)入package:flame/game.dart庫,這個(gè)庫里面包括我們現(xiàn)在使用的游戲(Game)抽象類,這個(gè)類有兩個(gè)方法:更新(update)和渲染(render),我們直接用同名方法覆蓋了它們。
在Dart 2.x版本中,@override注釋和new關(guān)鍵字是可選的,所以在這里也不需要寫。
接下來,我們?cè)?code>main.dart文件中創(chuàng)建BoxGame類的實(shí)例,并將其widget屬性傳遞給runApp函數(shù)。同時(shí),引用我們剛才創(chuàng)建的package:hello_flame/box-game.dart,讓BoxGame類可以在main.dart中使用。
...
import 'package:hello_flame/box-game.dart';
void main() async {
...
BoxGame game = BoxGame();
runApp(game.widget);
到這里為止,我們的main.dart里面應(yīng)該有以下代碼。
import 'package:flutter/material.dart';
import 'package:flame/util.dart';
import 'package:flutter/services.dart';
import 'package:hello_flame/box-game.dart';
void main() async {
Util flameUtil = Util();
await flameUtil.fullScreen();
await flameUtil.setOrientation(DeviceOrientation.portraitUp);
BoxGame game = BoxGame();
runApp(game.widget);
}
現(xiàn)在我們的應(yīng)用程序可以被稱為游戲了,運(yùn)行游戲,會(huì)顯示一個(gè)空白的黑屏,因?yàn)槲覀冞€沒有在屏幕上繪制具體的內(nèi)容。
屏幕的大小和尺寸
Flame這個(gè)游戲開發(fā)框架是以Flutter為基礎(chǔ)的,而Flutter在屏幕上繪制時(shí)使用邏輯像素,因此,我們?cè)贔lame上調(diào)整游戲?qū)ο蟮拇笮r(shí)也是使用邏輯像素。
實(shí)際上,游戲(Game)抽象類上有個(gè)調(diào)整(resize)方法,這個(gè)方法接受大?。?code>Size)類參數(shù),使用這個(gè)參數(shù)就可以確定設(shè)備的屏幕大小。
首先在box-game.dart文件中,添加一個(gè)BoxGame類的實(shí)例變量screenSize,這個(gè)變量用于保持屏幕的大小,只有當(dāng)屏幕的大小發(fā)生變化時(shí)才會(huì)更新,它也是Flame在屏幕上繪制對(duì)象時(shí)的基礎(chǔ)。screenSize是Size類型的變量,與傳遞給調(diào)整(resize)方法的參數(shù)一致。
類變量screenSize的初始值為null,可以用來判斷渲染過程中是否已知屏幕大小。接下來,我們編寫一個(gè)同名方法覆蓋調(diào)整(resize)方法。
class BoxGame extends Game {
Size screenSize;
...
void resize(Size size) {
screenSize = size;
super.resize(size);
}
到這里為止,我們的box-game.dart里面應(yīng)該有以下代碼。
import 'dart:ui';
import 'package:flame/game.dart';
class BoxGame extends Game {
Size screenSize;
void render(Canvas canvas) {
// TODO: 實(shí)現(xiàn)渲染
}
void update(double t) {
// TODO: 實(shí)現(xiàn)更新
}
void resize(Size size) {
screenSize = size;
super.resize(size);
}
}
繪制畫布和背景
到這一步,游戲主循環(huán)已經(jīng)存在,可以開始繪制一些對(duì)象了。在渲染(render)方法中,我們可以訪問畫布(Canvas),這個(gè)畫布(Canvas)是Flame提供的,在畫布(Canvas)上繪制游戲圖形之后,F(xiàn)lame會(huì)將其繪制并將整個(gè)畫布繪制到屏幕上。
在畫布上繪圖時(shí),就像我們拿著畫筆畫畫一樣,先繪制最底層的背景對(duì)象,然后在上面繪制一些動(dòng)物、植物或建筑物對(duì)象。
現(xiàn)在我們可以開始繪制背景,這個(gè)例子中游戲背景只是一個(gè)黑屏,可以使用以下代碼繪制。
void render(Canvas canvas) {
// TODO: 實(shí)現(xiàn)渲染
// 在整個(gè)屏幕上繪制黑色背景
Rect bgRect = Rect.fromLTWH(0, 0, screenSize.width, screenSize.height);
Paint bgPaint = Paint();
bgPaint.color = Color(0xff000000);
canvas.drawRect(bgRect, bgPaint);
上面代碼中,第一行聲明了一個(gè)與屏幕一樣大小的矩形(Rect),坐標(biāo)位于(0,0),即屏幕的左上角,我們就用這個(gè)當(dāng)游戲背景了。
然后,第二行聲明一個(gè)繪制(Paint)類對(duì)象,其后尾隨配置這個(gè)繪制(Paint)類對(duì)象的顏色(Color)。
最后一行代碼使用前面定義的矩形(Rect)和繪制(Paint)實(shí)例在畫布(Canvas)上繪制一個(gè)矩形。
繪制上層的對(duì)象
接下來的步驟中,我們會(huì)在屏幕的中間繪制一個(gè)游戲?qū)ο?,在?dāng)前游戲中,游戲?qū)ο笫且粋€(gè)小矩形圖案。
void render(Canvas canvas) {
...
// 畫一個(gè)盒子,如果獲勝則將其設(shè)為綠色,否則為白色
double screenCenterX = screenSize.width / 2;
double screenCenterY = screenSize.height / 2;
Rect boxRect = Rect.fromLTWH(
screenCenterX - 75,
screenCenterY - 75,
150,
150,
);
Paint boxPaint = Paint();
boxPaint.color = Color(0xffffffff);
canvas.drawRect(boxRect, boxPaint);
}
上面代碼中,前面2行代碼聲明兩個(gè)變量,分別是用于保持屏幕中心坐標(biāo)的變量,分別為屏幕寬度和高度的一半。
接下來的6行代碼聲明了一個(gè)150x150個(gè)邏輯像素大小的矩形,它位于屏幕中間,但是會(huì)向左偏移75個(gè)像素,向上偏移75個(gè)像素。
其余的代碼前面繪制畫布和背景的代碼差不多,此時(shí)運(yùn)行游戲,就可以看到黑色背景上有一個(gè)白色的矩形對(duì)象。
處理輸入和勝利條件
到這里,我們已經(jīng)完成了大部分內(nèi)容,現(xiàn)在只需要接受玩家的輸入了。在box-game.dart文件中,先導(dǎo)入Flutter的手勢(shì)庫(package:flutter/gestures.dart),然后還要添加點(diǎn)擊操作的處理函數(shù)。
...
import 'package:flutter/gestures.dart';
class BoxGame extends Game {
...
void onTapDown(TapDownDetails d) {
// 處理點(diǎn)擊
}
}
然后回到main.dart文件中,注冊(cè)一個(gè)手勢(shì)識(shí)別器(GestureRecognizer)并將其點(diǎn)擊(onTapDown)事件鏈接到游戲的點(diǎn)擊(onTapDown)處理程序。同時(shí),我們也不要忘記在這里導(dǎo)入Flutter的手勢(shì)庫(package:flutter/gestures.dart),以便在此文件中可以使用手勢(shì)識(shí)別器(GestureRecognizer)類。
再然后,定位到main函數(shù)內(nèi)部,聲明一個(gè)點(diǎn)擊手勢(shì)識(shí)別器(TapGestureRecognizer)并將其點(diǎn)擊(onTapDown)事件分配給游戲的點(diǎn)擊(onTapDown)處理程序。最后使用Flutter的工具庫package:flame/util.dart中的添加手勢(shì)識(shí)別器(addGestureRecognizer)函數(shù)注冊(cè)手勢(shì)識(shí)別器。
...
import 'package:flutter/gestures.dart';
void main() async {
...
BoxGame game = BoxGame();
TapGestureRecognizer tapper = TapGestureRecognizer();
tapper.onTapDown = game.onTapDown;
runApp(game.widget);
flameUtil.addGestureRecognizer(tapper);
}
到這里為止,我們的main.dart里面應(yīng)該有以下代碼。
import 'package:flutter/material.dart';
import 'package:flame/util.dart';
import 'package:flutter/services.dart';
import 'package:hello_flame/box-game.dart';
import 'package:flutter/gestures.dart';
void main() async {
Util flameUtil = Util();
await flameUtil.fullScreen();
await flameUtil.setOrientation(DeviceOrientation.portraitUp);
BoxGame game = BoxGame();
TapGestureRecognizer tapper = TapGestureRecognizer();
tapper.onTapDown = game.onTapDown;
runApp(game.widget);
flameUtil.addGestureRecognizer(tapper);
}
現(xiàn)在,我們?cè)倩氐?code>box-game.dart文件中來,添加另一個(gè)實(shí)例變量hasWon來判斷玩家是否勝利,定義一個(gè)布爾(bool)變量,默認(rèn)為false表示玩家未取得勝利。
然后在渲染(render)方法里面,寫一個(gè)條件判斷,如果玩家已經(jīng)勝利,將boxPaint的顏色設(shè)置成綠色,否則為白色。
class BoxGame extends Game {
...
bool hasWon = false;
void render(Canvas canvas) {
...
Paint boxPaint = Paint();
if (hasWon) {
boxPaint.color = Color(0xff00ff00);
} else {
boxPaint.color = Color(0xffffffff);
}
canvas.drawRect(boxRect, boxPaint);
}
...
}
最后我們還需要在游戲的點(diǎn)擊(onTapDown)處理程序中添加邏輯代碼,判斷玩家是否點(diǎn)擊了中間的矩形,如果是,就將hasWon變量的值轉(zhuǎn)換為true,表示玩家已經(jīng)取得勝利。
void onTapDown(TapDownDetails d) {
// 處理點(diǎn)擊
double screenCenterX = screenSize.width / 2;
double screenCenterY = screenSize.height / 2;
if (d.globalPosition.dx >= screenCenterX - 75 &&
d.globalPosition.dx <= screenCenterX + 75 &&
d.globalPosition.dy >= screenCenterY - 75 &&
d.globalPosition.dy <= screenCenterY + 75) {
hasWon = true;
}
}
上面代碼中,前面2行用來確定屏幕中心點(diǎn)的坐標(biāo),后面的5行多條件判斷的if語句,用來判斷點(diǎn)擊坐標(biāo)是否位于屏幕中間的150x150邏輯像素范圍內(nèi)。
如果是,就轉(zhuǎn)換hasWon變量的值,并在下次調(diào)用渲染(render)方法時(shí)反映在屏幕上。同時(shí)我們這里將更新(update)方法留空了,因?yàn)檫@個(gè)游戲里不會(huì)更新任何內(nèi)容呀。
到這里為止,我們的box-game.dart里面應(yīng)該有以下代碼。
import 'dart:ui';
import 'package:flutter/gestures.dart';
import 'package:flame/game.dart';
class BoxGame extends Game {
Size screenSize;
bool hasWon = false;
void render(Canvas canvas) {
// 在整個(gè)屏幕上繪制黑色背景
Rect bgRect = Rect.fromLTWH(0, 0, screenSize.width, screenSize.height);
Paint bgPaint = Paint();
bgPaint.color = Color(0xff000000);
canvas.drawRect(bgRect, bgPaint);
// 畫一個(gè)盒子,如果獲勝則將其設(shè)為綠色,否則為白色
double screenCenterX = screenSize.width / 2;
double screenCenterY = screenSize.height / 2;
Rect boxRect = Rect.fromLTWH(
screenCenterX - 75,
screenCenterY - 75,
150,
150,
);
Paint boxPaint = Paint();
if (hasWon) {
boxPaint.color = Color(0xff00ff00);
} else {
boxPaint.color = Color(0xffffffff);
}
canvas.drawRect(boxRect, boxPaint);
}
void update(double t) {
// TODO: 實(shí)現(xiàn)更新
}
void resize(Size size) {
screenSize = size;
super.resize(size);
}
void onTapDown(TapDownDetails d) {
// 處理點(diǎn)擊
double screenCenterX = screenSize.width / 2;
double screenCenterY = screenSize.height / 2;
if (d.globalPosition.dx >= screenCenterX - 75 &&
d.globalPosition.dx <= screenCenterX + 75 &&
d.globalPosition.dy >= screenCenterY - 75 &&
d.globalPosition.dy <= screenCenterY + 75) {
hasWon = true;
}
}
}
運(yùn)行游戲,可以看到效果如下所示。
