基于Flutter和Flame游戲開發(fā)引擎學(xué)習(xí)資料觉既,Create a Mobile Game with Flutter and Flame – Beginner Tutorial崩泡,對于更加適用于復(fù)雜一點(diǎn)手機(jī)游戲的Flutter 開發(fā)的2D游戲引擎SpriteWidget也制作了boxGame游戲例子嘀倒,希望通過起始能夠?qū)τ赟priteWidget有比較好的了解和掌握,如果能夠幫助到你殿遂,別忘了給我點(diǎn)贊哦冗懦!
如果你有什么問題,可以發(fā)郵件給我溅呢,或者在Github上留言,方便的話猿挚,我會(huì)盡力幫助你咐旧。
你在了解的過程,也可以參考下面的文章:
1. Create a Mobile Game with Flutter and Flame – Beginner Tutorial
2.飛行射擊游戲spritewidget/spaceblast
3.Flutter高性能復(fù)雜游戲2D開發(fā)游戲引擎spritewidget
前提條件:
1. Android Studio - Flutter開發(fā)工具绩蜻,當(dāng)然你也可以使用其他的铣墨,但本例使用AS開發(fā)。
2.Flutter SDK/Framework - AS開發(fā)插件办绝,你需要具備開發(fā)Flutter的AS開發(fā)環(huán)境伊约,如果你還沒有掌握Flutter的開發(fā)基礎(chǔ),請先嘗試Flutter開發(fā)學(xué)習(xí)孕蝉。
你可以在Github找到這個(gè)練習(xí)的完整代碼屡律。
開始擼代碼吧:
Step 1: 創(chuàng)建一個(gè)Flutter Application(略,希望你是了解Flutter的)
Step 2: 添加spritewidget插件以及清理Application
打開./pubspec.yamland降淮,增加下面的內(nèi)容在thecupertino_icons行下面并且在derdependencies分類之下(注意縮進(jìn)).
spritewidget:
然后記得執(zhí)行flutter packages get超埋,或者點(diǎn)擊AS界面上的Packages Get來添加插件。
下一步是清理代碼佳鳖,F(xiàn)lutter Project新創(chuàng)建的是一個(gè)example霍殴,打開./lib/main.dart,清空代碼只保留void main() {}系吩,并且確保使用material library來運(yùn)行runApp() .
然后刪除./test目錄繁成,不然會(huì)出現(xiàn)錯(cuò)誤,這里我們用不上test方法淑玫。
Step 3:? 添加主程序
打開./lib/main.dart,添加一下代碼面睛,和Flutter創(chuàng)建一個(gè)新的界面主程序一樣:
import 'package:flutter/material.dart';
import 'package:spritewidget/spritewidget.dart';
main () async {
? runApp(MyApp());
}
class MyAppextends StatelessWidget {
? @override
? Widget build(BuildContext context) {
??? return MaterialApp(
????? title:"spriteWidget Game Eniger",
????? theme: ThemeData(
????? primarySwatch: Colors.blue,
????? ),
????? home: BoxGameScene(),
??? );
? }
}
class BoxGameSceneextends StatefulWidget {
? @override
? State createState() => BoxGameSceneState();
}
class BoxGameSceneStateextends State {
? void initState(){
??? super.initState();
? }
? @override
? Widget build(BuildContext context) {
??? return null;
? }
}
這樣就創(chuàng)建了一個(gè)Flutter的主程序絮蒿。
Step 4: 添加游戲根節(jié)點(diǎn)RootNode:
新建一個(gè)dart文件命名為box_game.dart,創(chuàng)建class繼承NodeWithSize叁鉴,import游戲包:
import 'package:spritewidget/spritewidget.dart';
class BoxGame extends NodeWithSize {
? BoxGame() :super(new Size(320.0,320.0)){
? }
}
說明:創(chuàng)建一個(gè)BoxGame集成NodeWithSize作為RootNode土涝,這樣就可以實(shí)現(xiàn)spritewidget游戲引擎的游戲loop。一個(gè)基本的游戲loop在NodeWithSize以及繼承的Node中可以通過update()和paint()方法來實(shí)現(xiàn)幌墓。
update()方法實(shí)現(xiàn)游戲node的移動(dòng)或者更新(比如timer)但壮。
paint()方法實(shí)現(xiàn)游戲node的繪制冀泻。
這里有一個(gè)非常重要的概念,spritewidget產(chǎn)生了自己的cooridinate system蜡饵,使用Node自身坐標(biāo)系進(jìn)行對象位置的處理弹渔,因此:
BoxGame() :super(new Size(320.0,320.0)){}
就是給BoxGame這個(gè)Node初始化一個(gè)320*320尺寸的作為Node的自身坐標(biāo)系,這樣就可以按照這個(gè)坐標(biāo)系進(jìn)行新的子Node(child)的添加溯祸,每個(gè)Node肢专,無論parent/child,都可以有自己自身的坐標(biāo)系焦辅,以及起始位置相對于parent的坐標(biāo)位置博杖。
這個(gè)會(huì)在文后詳細(xì)的把自己研究的坐標(biāo)系相關(guān)信息進(jìn)行描述。
Step 4: 添加RootNode到主程序
在main.dart中引入box_game.dart筷登,然后在主程序app的state中初始化一個(gè)NodeWithSize剃根,然后返回spritewidget():
import 'package:boxgamespritewidget/box_game.dart';
NodeWithSize _game;
_game =new BoxGame();
return SpriteWidget(_game);
這是main.dart就像下面的代碼:
import 'package:flutter/material.dart';
import 'package:spritewidget/spritewidget.dart';
import 'package:boxgamespritewidget/box_game.dart';
main () async {
? runApp(MyApp());
}
class MyAppextends StatelessWidget {
? @override
? Widget build(BuildContext context) {
??? return MaterialApp(
????? title:"spriteWidget Game Eniger",
????? theme: ThemeData(
????? primarySwatch: Colors.blue,
????? ),
????? home: BoxGameScene(),
??? );
? }
}
class BoxGameSceneextends StatefulWidget {
? @override
? State createState() => BoxGameSceneState();
}
class BoxGameSceneStateextends State {
? NodeWithSize _game;
? void initState(){
??? super.initState();
??? _game =new BoxGame();
? }
? @override
? Widget build(BuildContext context) {
??? return SpriteWidget(_game);
? }
}
這時(shí)候你的app已經(jīng)是個(gè)游戲app了。你可以運(yùn)行一下看看前方。
Step 5: 繪制游戲主界面
添加游戲主節(jié)點(diǎn)
spritewidget添加對象非常簡單狈醉,只需要在parentNode中addChild(childNode)就可以了,當(dāng)然镣丑,需要你首先定義childNode舔糖。
我們在RootNode中添加一個(gè)游戲主界面_gameScreen,打開box_game.dart, 在BoxGame初始化時(shí)添加childNode莺匠。
Node_gameScreen;
BoxGame() :super(new Size(320.0,320.0)){
? _gameScreen =new Node();
? addChild(_gameScreen);
}
在rootNode中添加一個(gè)_gameScreen金吗,是為了更好的在各Node進(jìn)行交互時(shí)能夠更有層次,方便不同parentNode和childNode的不同更新和繪制趣竣。
添加虛擬游戲操縱桿
spritewidget有一個(gè)炫酷的Node稱之為VirtualJoystick摇庙,可以通過這個(gè)組件來控制游戲?qū)ο蟮妮斎耄热鐐€(gè)方向在按住屏幕是的移動(dòng)遥缕。
打開box_game.dart卫袒,相同于添加_gameScreen的方法,但是把_gameScreen作為父節(jié)點(diǎn)单匣,將VirtualJoystick添加到_gameScreen中夕凝,
VirtualJoystick_joystick;
BoxGame() :super(new Size(320.0,320.0)){
? _gameScreen =new Node();
? addChild(_gameScreen);
? _joystick =new VirtualJoystick();
? _gameScreen.addChild(_joystick);
}
這時(shí)候你可以運(yùn)行程序看看,你會(huì)發(fā)現(xiàn)VirtualJoystick并不存在户秤,怎么回事码秉?
原來,我們定義的RootNode使用了一個(gè)size(320, 320)的區(qū)域作為自己的坐標(biāo)系統(tǒng)鸡号,那么對于不同尺寸的手機(jī)屏幕转砖,并不是320x320的,這樣就會(huì)根據(jù)rootNode的定義區(qū)域?qū)⑵聊贿M(jìn)行適配鲸伴,這時(shí)候VirtualJoystick缺省會(huì)被添加到屏幕的最下方府蔗,因此晋控,我們需要初始化320x320
上的_gameScreen,設(shè)置它為最下端的屏幕Node姓赤,需要重新設(shè)置_gameScreen.position,
打開box_game.dart赡译,在class BoxGame 中override方法spriteBoxPerformedLayout(),
@override
void spriteBoxPerformedLayout() {
? _gameScreen.position =new Offset(0.0,spriteBox.visibleArea.height);
}
讓_gameScreen.position設(shè)置為基于ParentNode的高度方向坐標(biāo)為y設(shè)置為spriteBox.visibleArea.height模捂, 也就是320捶朵,向上平移320的高度。spriteBox.visibleArea之的是屏幕可見部分在320x320父節(jié)點(diǎn)中顯示的部分狂男,具體值可以參考后續(xù)說明综看。
這時(shí)再運(yùn)行程序,一個(gè)很不錯(cuò)的游戲操縱桿在界面上顯示了岖食。
繪制并添加一個(gè)Box
新建一個(gè)BoxNode類集成Node红碑,然后在繪制一個(gè)正方形,打開box_gam.dart泡垃,在BoxGame類下面創(chuàng)建一個(gè)BoxNode類析珊,也可以新生成一個(gè)dart文件來創(chuàng)建BoxNode,然后在box_gam.dart中引用蔑穴。
class BoxNode extends Node {
? BoxNode() {
??? position = new Offset(0, 0);
? }
? @override
? void paint(Canvas canvas) {
??? ???
? }
}
和BoxGame一樣忠寻,BoxNode除了繼承Node類以外,同樣可以初始化相對于parentNode的position = new Offset(0,0); 通過paint()和update()進(jìn)行繪制和更新存和。
說明:NodeWithSize實(shí)際上繼承Node奕剃,但是增加了size和pivot點(diǎn),可以通過更好的尺寸和支點(diǎn)來對Node進(jìn)行更新和繪制捐腿。適合作為ParentNode或者RootNode纵朋,各個(gè)游戲?qū)ο罂梢允褂肗ode創(chuàng)建。
在BoxNode中使用paint()進(jìn)行繪制一個(gè)正方形茄袖,
@override
void paint(Canvas canvas) {
? boxRect = Rect.fromLTWH(
????? spriteBox.visibleArea.height / 2 - 15,
????? - spriteBox.visibleArea.height / 2 - 75,
????? 30,
????? 30,
? );
? Paint boxPaint = Paint();
? boxPaint.color = Color(0xff00ff00);
? canvas.drawRect(boxRect, boxPaint);
}
說明:基本上來講操软,如果child的中點(diǎn)就是parent的size的中點(diǎn)減去偏移(前提是child的position初始化為new Offset(0, 0)。繪制對象使用canvas進(jìn)行宪祥,對象和Paint()就可以實(shí)時(shí)繪制對象聂薪。
然后將BoxNode實(shí)例化,并添加到BoxGame的_gameScreen游戲主節(jié)點(diǎn)中蝗羊,在BoxGame類的BoxGame() :super(new Size(320.0,320.0)){}初始化的_gameScreen下面添加胆建,
BoxNode_box;
BoxGame() : super(new Size(320.0, 320.0)){
? _gameScreen = new Node();
? addChild(_gameScreen);
? _joystick = new VirtualJoystick();
? _gameScreen.addChild(_joystick);
? _box = new BoxNode();
? _gameScreen.addChild(_box);
}
OK,這時(shí)候運(yùn)行程序肘交,一個(gè)綠色的box以及一個(gè)虛擬游戲操縱桿就會(huì)出現(xiàn)在游戲主界面。
Step 6: 處理虛擬游戲操作
我們要通過虛擬游戲操縱桿來控制box的移動(dòng)扑馁,需要在主界面update()中來通過VirtualJoystick來根據(jù)VirtualJoystick.value來改變box.position.
給BoxNode添加根據(jù)VirtualJoystick.value來更新自身position的方法涯呻,在class BoxNode中添加凉驻,
void applyThrust(Offset joystickValue) {
? Offset oldPos = position;
? Offset target = new Offset(joystickValue.dx * 160.0, joystickValue.dy * 220.0);
? double filterFactor = 0.2;
? position = new Offset(
? ? ? GameMath.filter(oldPos.dx, target.dx, filterFactor),
? ? ? GameMath.filter(oldPos.dy, target.dy, filterFactor));
}
說明:可以看到首先獲取BoxNode的當(dāng)前position作為舊的oldPos, 然后根據(jù)VirtualJoystick.value計(jì)算VirtualJoystick滑動(dòng)屏幕的移動(dòng)亮并根據(jù)屏幕尺寸進(jìn)行放大,這里使用x放大160复罐,y放大220涝登,基本上是根據(jù)測試結(jié)果,會(huì)讓VirtualJoystick操作box時(shí)看著比較流暢效诅。
GameMath.filter方法可以在oldPos和target之間按照一個(gè)0-1之間的filterFactor插入多個(gè)移動(dòng)位置胀滚,而不是直接將Box對象從oldPos移動(dòng)到target,這樣在更新的時(shí)候就會(huì)產(chǎn)生Box連續(xù)移動(dòng)的效果乱投,移動(dòng)效果會(huì)比較流暢咽笼。
然后在BoxGame中,override主游戲RootNode的update()方法戚炫,將box的position的改變進(jìn)行更新剑刑,這樣就可以是的Box在屏幕上根據(jù)VirtualJoystick的操作進(jìn)行移動(dòng),
void update(double dt) {
? _box.applyThrust(_joystick.value);
}
這樣整個(gè)box_game.dart就像下面的代碼双肤,
import 'dart:ui';
import 'package:flutter/gestures.dart';
import 'package:spritewidget/spritewidget.dart';
class BoxGame extends NodeWithSize {
? // Game screen nodes
? Node _gameScreen;
? VirtualJoystick _joystick;
? BoxNode _box;
? double _scroll = 0.0;
? BoxGame() : super(new Size(320.0, 320.0)){
? ? _gameScreen = new Node();
? ? addChild(_gameScreen);
? ? _joystick = new VirtualJoystick();
? ? _gameScreen.addChild(_joystick);
? ? _box = new BoxNode();
? ? _gameScreen.addChild(_box);
? }
? @override
? void spriteBoxPerformedLayout() {
? ? _gameScreen.position = new Offset(0.0, spriteBox.visibleArea.height);
? }
? void update(double dt) {
? ? _box.applyThrust(_joystick.value);
? }
}
class BoxNode extends Node {
? bool hasWon = false;
? Rect boxRect;
? BoxNode() {
? ? position = new Offset(0, 0);
? }
? @override
? void paint(Canvas canvas) {
? ? boxRect = Rect.fromLTWH(
? ? ? ? spriteBox.visibleArea.height / 2 - 15,
? ? ? ? - spriteBox.visibleArea.height / 2 - 75,
? ? ? ? 30,
? ? ? ? 30,
? ? );
? ? Paint boxPaint = Paint();
? ? boxPaint.color = Color(0xff00ff00);
? ? canvas.drawRect(boxRect, boxPaint);
? }
? void applyThrust(Offset joystickValue) {
? ? Offset oldPos = position;
? ? Offset target = new Offset(joystickValue.dx * 160.0, joystickValue.dy * 220.0);
? ? double filterFactor = 0.2;
? ? position = new Offset(
? ? ? ? GameMath.filter(oldPos.dx, target.dx, filterFactor),
? ? ? ? GameMath.filter(oldPos.dy, target.dy, filterFactor));
? }
}
好了施掏,運(yùn)行一下程序,你可以通過VirtualJoystick來控制Box的移動(dòng)了茅糜,是不是很酷七芭?
Step 7: 添加點(diǎn)擊事件控制box變色展示win狀態(tài)
我們希望游戲?qū)ο罂梢员稽c(diǎn)擊操作,spritewidget使用handleEvent(SpriteBoxEvent event){}來處理輸入交互蔑赘,SpriteBoxEvent包含多種點(diǎn)擊屏幕處理事件狸驳,我們這里需要使用PointerDownEvent事件,當(dāng)Box被點(diǎn)中時(shí)進(jìn)行變色米死。
如果需要實(shí)現(xiàn)點(diǎn)擊事件锌历,需要對于點(diǎn)擊檢測Node進(jìn)行設(shè)置,允許點(diǎn)擊檢測對象可以進(jìn)行交互峦筒,但是不允許多點(diǎn)碰觸究西,我們希望通過對BoxGame作為RootNode進(jìn)行對象檢測,如果發(fā)現(xiàn)在RootNode中點(diǎn)擊到的區(qū)域是box的區(qū)域物喷,則表明box被點(diǎn)中了卤材,然后處理box被點(diǎn)擊方法,首先在BoxGame初始化的時(shí)候BoxGame() :super(new Size(320.0,320.0)){}設(shè)置BoxGame定義為峦失,
userInteractionEnabled =true;
handleMultiplePointers =false;
然后在BoxGame類中添加override方法扇丛,
@override
bool handleEvent(SpriteBoxEvent event) {
? if (event.type == PointerDownEvent) {
? ? Offset newPoint = convertPointToNodeSpace(event.boxPosition);
? ? if(newPoint.dx > _box.boxRect.left &&
? ? ? ? newPoint.dx < _box.boxRect.left + 30 &&
? ? ? ? newPoint.dy > _box.boxRect.top + spriteBox.visibleArea.height &&
? ? ? ? newPoint.dy < _box.boxRect.top + spriteBox.visibleArea.height + 30){
? ? ??? //do box actions.
? ? }else{
? ? ? return false;
? ? }
? }
? return true;
}
說明:當(dāng)點(diǎn)擊事件發(fā)生時(shí),判斷是否為屏幕被點(diǎn)中尉辑,這是由于點(diǎn)中的位置為屏幕缺省坐標(biāo)點(diǎn)帆精,如果需要和RootNode的childNode的范圍進(jìn)行比較,需要首先使用convertPointToNodeSpace()方法將缺省坐標(biāo)點(diǎn)轉(zhuǎn)換為RootNode坐標(biāo)系統(tǒng),然后再和box的范圍進(jìn)行比較卓练,由于_gameScreen作為主Node隘蝎,初始化為new Offset(0.0, spriteBox.visibleArea.height),所以在對點(diǎn)擊的位置判斷是襟企,需要把_box的位置也添加相同的偏移進(jìn)行對比嘱么。
添加點(diǎn)擊位置判斷是box范圍時(shí),處理box的方法顽悼,在BoxNode類中定義曼振,
bool hasWon =false;
定義一個(gè)布爾參數(shù)來分辨點(diǎn)擊每次的狀態(tài),然后在paint()函數(shù)中蔚龙,更改
boxPaint.color = Color(0xff00ff00);
布爾參數(shù)判讀來使用不同顏色畫筆冰评,
if (hasWon) {
? boxPaint.color = Color(0xff00ff00);
} else {
? boxPaint.color = Color(0xffffffff);
}
如果布爾參數(shù)為true,畫筆為綠色府蛇,否則為白色集索,然后定義一個(gè)點(diǎn)擊調(diào)用的方法,來根據(jù)點(diǎn)擊改變布爾參數(shù)的值汇跨,
void onTapDown() {
? hasWon = !hasWon;
}
box被點(diǎn)中一次布爾參數(shù)為true务荆,再點(diǎn)擊變?yōu)閒alse,這樣不同的點(diǎn)擊布爾參數(shù)就會(huì)有不同的狀態(tài)穷遂,這樣就會(huì)在paint()方法中使用不同顏色的畫筆來繪制box函匕。
然后在BoxGame的bool handleEvent(SpriteBoxEvent event) {} 方法中判斷點(diǎn)中box時(shí)調(diào)用onTapDown()方法,替換
//do box actions.
為
_box.onTapDown();
這樣就完成了點(diǎn)擊輸入方式交互蚪黑,整體box_game.dart的代碼如下盅惜,
import 'dart:ui';
import 'package:flutter/gestures.dart';
import 'package:spritewidget/spritewidget.dart';
class BoxGame extends NodeWithSize {
? Node _gameScreen;
? VirtualJoystick _joystick;
? BoxNode _box;
? double _scroll = 0.0;
? BoxGame() : super(new Size(320.0, 320.0)){
??? userInteractionEnabled = true;
??? handleMultiplePointers = false;
??? _gameScreen = new Node();
??? addChild(_gameScreen);
??? _joystick = new VirtualJoystick();
??? _gameScreen.addChild(_joystick);
??? _box = new BoxNode();
??? _gameScreen.addChild(_box);
? }
? @override
? bool handleEvent(SpriteBoxEvent event) {
??? if (event.type == PointerDownEvent) {
????? Offset newPoint = convertPointToNodeSpace(event.boxPosition);
????? if(newPoint.dx > _box.boxRect.left &&
????????? newPoint.dx < _box.boxRect.left + 30 &&
????????? newPoint.dy > _box.boxRect.top + spriteBox.visibleArea.height &&
????????? newPoint.dy < _box.boxRect.top + spriteBox.visibleArea.height + 30){
??????? _box.onTapDown();
????? }else{
??????? return false;
????? }
??? }
??? return true;
? }
? @override
? void spriteBoxPerformedLayout() {
??? _gameScreen.position = new Offset(0.0, spriteBox.visibleArea.height);
? }
? void update(double dt) {
??? _box.applyThrust(_joystick.value);
? }
}
class BoxNode extends Node {
? bool hasWon = false;
? Rect boxRect;
? BoxNode() {
??? position = new Offset(0, 0);
? }
? @override
? void paint(Canvas canvas) {
??? boxRect = Rect.fromLTWH(
??????? spriteBox.visibleArea.height / 2 - 15,
??????? - spriteBox.visibleArea.height / 2 - 75,
??????? 30,
??????? 30,
??? );
???
??? Paint boxPaint = Paint();
??? boxPaint.color = Color(0xffffffff);
??? if (hasWon) {
????? boxPaint.color = Color(0xff00ff00);
??? } else {
????? boxPaint.color = Color(0xffffffff);
??? }
??? canvas.drawRect(boxRect, boxPaint);
? }
? void onTapDown() {
??? hasWon = !hasWon;
? }
? void applyThrust(Offset joystickValue) {
??? Offset oldPos = position;
??? Offset target = new Offset(joystickValue.dx * 160.0, joystickValue.dy * 220.0);
??? double filterFactor = 0.2;
??? position = new Offset(
??????? GameMath.filter(oldPos.dx, target.dx, filterFactor),
??????? GameMath.filter(oldPos.dy, target.dy, filterFactor));
? }
}
好了你可以體驗(yàn)一下這個(gè)游戲了。
關(guān)于Node的坐標(biāo)系統(tǒng)和世界坐標(biāo)系統(tǒng)的相關(guān)聯(lián)忌穿,后期添加抒寂。
你可以在Github找到這個(gè)練習(xí)的完整代碼。