TankCombat系列文章
如果你還不了解Flame可以看這里:
見(jiàn)微知著,F(xiàn)lutter在游戲開(kāi)發(fā)的表現(xiàn)及跨平臺(tái)帶來(lái)的優(yōu)勢(shì)
Flutter&Flame——TankCombat游戲開(kāi)發(fā)(一)
Flutter&Flame——TankCombat游戲開(kāi)發(fā)(二)
Flutter&Flame——TankCombat游戲開(kāi)發(fā)(三)
Flutter&Flame——TankCombat游戲開(kāi)發(fā)(四)
游戲介紹
玩法
我們要實(shí)現(xiàn)一個(gè)坦克大戰(zhàn):
玩家控制藍(lán)色坦克,出生于屏幕中間
綠色和黃色為敵軍坦克,出生于屏幕四角(隨機(jī))
發(fā)射的炮彈可以擊毀坦克
敵軍坦克在被摧毀后,會(huì)隨機(jī)重生,但總體敵軍數(shù)量保持4個(gè)
坦克可以發(fā)射炮彈,并分別旋轉(zhuǎn)坦克身體和炮塔
更多功能待發(fā)現(xiàn)...
效果圖

開(kāi)工
一口吃不了一個(gè)胖子,我們將項(xiàng)目拆分,先實(shí)現(xiàn)背景、搖桿和繪制一輛坦克
搖桿主要借鑒自官方,如果你已經(jīng)在官方的教程里學(xué)會(huì)了,可以略過(guò)此章
準(zhǔn)備
首先我們引入Flame插件
flame: ^0.24.0
之后添加背景圖片資源文件:
assets/images/
[圖片上傳失敗...(image-3818fa-1597136568141)]
開(kāi)始代碼部分,我們將main函數(shù)清空,如下:
main()async{
}
添加如下代碼,(還是老規(guī)矩,代碼多時(shí)我會(huì)將說(shuō)明添加到注解里。)
void main()async{
//確保flutter啟動(dòng)成功
WidgetsFlutterBinding.ensureInitialized();
//為flame加載資源文件
loadAssets();
///設(shè)置橫屏
await SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeRight,
DeviceOrientation.landscapeLeft
]);
///全面屏
await SystemChrome.setEnabledSystemUIOverlays([]);
//這個(gè)稍后解釋
final TankGame tankGame = TankGame();
runApp(...)//這里下面詳細(xì)交代
}
loadAssets();的代碼如下,主要是加載圖片資源以備開(kāi)發(fā)時(shí)候的使用
void loadAssets(){
Flame.images.loadAll([
'new_map.webp',
]);
}
接下來(lái)的tankgame,我們需要說(shuō)一下它的父類(lèi)Game
Game
Game是Flame的核心,也是我們游戲的驅(qū)動(dòng)力,它內(nèi)部有兩個(gè)主要的方法,就是上篇文章提到的
render(Canvas c)和update(double t)
這里再貼一下官方的流程圖:
我們實(shí)際開(kāi)發(fā)時(shí),需要繼承它并在上面兩個(gè)方法中做我們自己的處理,如這里的TankGame:
class TankGame extends Game{
@override
void render(Canvas canvas) {}
@override
void update(double t) {}
///resize 這里的方法在屏幕尺寸變動(dòng)和第一次初始化時(shí)會(huì)調(diào)用,
///我們可以在這里獲取到屏幕的尺寸
@override
void resize(Size size) {}
}
接下來(lái)看最后一行的runApp(...)
runApp(...)
這里如app開(kāi)發(fā)一樣,是我們要加載widget的地方,可以看一下game里面有個(gè)widget變量,就是在這里面用的,不過(guò)現(xiàn)在我們先考慮一下布局。
通過(guò)觀察,可以發(fā)現(xiàn)搖桿是懸浮于地圖上方的,所以這里用stack比較合適。代碼如下:
runApp(Directionality(textDirection: TextDirection.ltr,
child: Stack(
children: [
///我們將游戲內(nèi)容如tank,地圖等放在最底層
tankGame.widget,
//上層放搖桿
Column(
children: [
//這個(gè)widget可以將搖桿擠在底部,內(nèi)部是一個(gè)Expanded
Spacer(),
//兩個(gè)發(fā)射按鈕 位于屏幕兩端
Row(
children: [
SizedBox(width: 48),
FireButton(
onTap: tankGame.onFireButtonTap,
),
Spacer(),
FireButton(
onTap: tankGame.onFireButtonTap,
),
SizedBox(width: 48),
],
),
//讓發(fā)射按鈕和搖桿保持一定間距
SizedBox(height: 20),
//兩個(gè)搖桿 位于屏幕兩端,發(fā)射按鈕下方
Row(
children: [
SizedBox(width: 48),
JoyStick(
onChange: (Offset delta)=>tankGame.onLeftJoypadChange(delta),
),
Spacer(),
JoyStick(
onChange: (Offset delta)=>tankGame.onRightJoypadChange(delta),
),
SizedBox(width: 48)
],
),
SizedBox(height: 24)
],
),
],
)));
這樣我們的基本布局就算完成了,先對(duì)布局結(jié)構(gòu)有一個(gè)了解,具體內(nèi)部什么樣子,我們一步一步來(lái)。
Component&Sprite
在游戲開(kāi)發(fā)前,我們需要先簡(jiǎn)單了解一下兩個(gè)東西
component : 組件(我覺(jué)得它跟游戲開(kāi)發(fā)中的 剛體 很像),如子彈、坦克等游戲角色都屬于component
sprite : 這個(gè)內(nèi)部方法很簡(jiǎn)單,主要是將圖形繪制在游戲界面上
由component的定義可以知道,它與游戲的每幀都有關(guān)系,因此需要增加兩個(gè)與game的update和render對(duì)應(yīng)的方法,為了便于理解,我們依然為component的這兩個(gè)方法命名為:update和render,同時(shí)抽出來(lái):
abstract class BaseComponent{
void render(Canvas canvas);
void update(double t);
}
搞定! 下面再來(lái)布置一下我們的Game(TankGame)
TankGame
TankGame繼承自Game,我們從這里可以獲得游戲場(chǎng)景大小,同時(shí)通過(guò)update和render驅(qū)動(dòng)各個(gè)component,代碼如下:
class TankGame extends Game{
//用來(lái)保存游戲場(chǎng)景尺寸
Size screenSize;
//游戲背景
BattleBackground bg;
@override
void render(Canvas canvas) {
//沒(méi)有初始化成功的話,不進(jìn)行繪制
if(screenSize == null)return;
//繪制背景
bg.render(canvas);
}
@override
void update(double t) {
if(screenSize == null)return;
}
///resize 這里的方法在屏幕尺寸變動(dòng)和第一次初始化時(shí)會(huì)調(diào)用,
///我們可以在這里獲取到屏幕的尺寸
@override
void resize(Size size) {
screenSize = size;
//初始化一個(gè)背景sprite
if(bg == null){
bg = BattleBackground(this);
}
}
}
我們?cè)趃ame里保存下場(chǎng)景尺寸,并且初始化一個(gè)bg,同時(shí)在render里調(diào)用bg的render方法,將背景繪制到游戲上,讓我們看一下BattleBackground
背景
背景(BattleBackground)實(shí)現(xiàn)非常簡(jiǎn)單,它的代碼如下:
class BattleBackground with BaseComponent{
final TankGame game;
Sprite bgSprite;
Rect bgRect;
BattleBackground(this.game){
//將bgSprite初始化,并將地圖圖片引入進(jìn)來(lái)
bgSprite = Sprite('new_map.webp');
//根據(jù)游戲場(chǎng)景尺寸確定一個(gè)rect,用來(lái)告訴sprite繪制區(qū)域
bgRect = Rect.fromLTWH(0, 0, game.screenSize.width, game.screenSize.height);
}
@override
void render(Canvas canvas) {
bgSprite.renderRect(canvas, bgRect);
}
@override
void update(double t) {
}
}
因?yàn)樵蹅兊牡貓D目前并沒(méi)有什么變化,所以u(píng)pdate方法可以不管,只需要render里繪制一下即可。
這里的大致流程是,game啟動(dòng)后,會(huì)循環(huán)調(diào)用下面的方法:
(TankGame)update->render->update->...
我們?cè)趃ame的render中調(diào)用背景的render方法,就可以繪制圖片了。
至此,背景就添加成功了,下面我們制作搖桿
搖桿
我們這里要用到widget,起名叫JoyStick。如果你會(huì)flutter開(kāi)發(fā),那么接下來(lái)的代碼是非常簡(jiǎn)單的。
首先聲明一個(gè)JoyStick
class JoyStick extends StatefulWidget{
//用于回傳搖桿移動(dòng)的方位
final void Function(Offset) onChange;
const JoyStick({Key key, this.onChange}) : super(key: key);
@override
State<StatefulWidget> createState() {
return JoyStickState();
}
}
class JoyStickState extends State<JoyStick> {}
state內(nèi)部實(shí)現(xiàn)如下,代碼比較多我將說(shuō)明寫(xiě)在注釋里
class JoyStickState extends State<JoyStick> {
//搖桿中間的圓的位置,簡(jiǎn)稱(chēng) 搖桿頭
Offset delta = Offset.zero;
//更新 搖桿頭的位置,并將位置傳出去(這樣就可以控制坦克了)
void updateDelta(Offset newD){
widget.onChange(newD);
setState(() {
delta = newD;
});
}
//這個(gè)是根據(jù)用戶(hù)移動(dòng)搖桿頭時(shí)的控制計(jì)算,主要是確保搖桿頭的活動(dòng)范圍不能超出 外層白圈
void calculateDelta(Offset offset){
Offset newD = offset - Offset(bgSize/2,bgSize/2);
updateDelta(Offset.fromDirection(newD.direction,min(bgSize/4, newD.distance)));//活動(dòng)范圍控制在bgSize之內(nèi)
}
//搖桿外層的白圈尺寸,搖桿頭的尺寸跟這個(gè)也有關(guān)系
final double bgSize = 120;
@override
Widget build(BuildContext context) {
return SizedBox(
width: bgSize,height: bgSize,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(bgSize/2)
),
//監(jiān)聽(tīng)用戶(hù)手勢(shì)
child: GestureDetector(
///搖桿底部白圈
child: Container(
decoration: BoxDecoration(
color: Color(0x88ffffff),
borderRadius: BorderRadius.circular(bgSize/2),
),
child: Center(
child: Transform.translate(offset: delta,
///搖桿頭
child: SizedBox(
width: bgSize/2,height: bgSize/2,
child: Container(
decoration: BoxDecoration(
color: Color(0xccffffff),
borderRadius: BorderRadius.circular(30),
),
),
),),
),
),
onPanDown: onDragDown,
onPanUpdate: onDragUpdate,
onPanEnd: onDragEnd,
),
),
);
}
//三個(gè)方法主要用于獲取用戶(hù)觸摸位置的數(shù)據(jù)
void onDragDown(DragDownDetails d) {
calculateDelta(d.localPosition);
}
void onDragUpdate(DragUpdateDetails d) {
calculateDelta(d.localPosition);
}
void onDragEnd(DragEndDetails d) {
updateDelta(Offset.zero);
}
}
這樣搖桿部分就完了,回看runApp內(nèi)的方法,這個(gè)時(shí)候運(yùn)行一下就可以看到屏幕上面有個(gè)搖桿了。
按鈕
就是倆白圈,我直接上代碼了:
class FireButton extends StatelessWidget {
final void Function() onTap;
const FireButton({Key key, this.onTap}) : super(key: key);
@override
Widget build(BuildContext context) {
return SizedBox(
height: 64,width: 64,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(32)
),
child: GestureDetector(
child:Container(
decoration: BoxDecoration(
color: Color(0x88ffffff),
borderRadius: BorderRadius.circular(32),
),
),
onTap: onTap,
),
),
);
}
}
接下來(lái)我們開(kāi)始繪制坦克
繪制坦克
首先我們需要坦克的圖片,并加載進(jìn)flame.
別忘了在pub中添加,并get一下
之后回到main函數(shù)中的loadAssets()方法,加載剛才的圖片資源:
void loadAssets(){
Flame.images.loadAll([
'new_map.webp',
'tank/t_body_blue.webp',
'tank/t_turret_blue.webp',
'tank/t_body_green.webp',
'tank/t_turret_green.webp',
'tank/t_body_sand.webp',
'tank/t_turret_sand.webp',
'tank/bullet_blue.webp',
'tank/bullet_green.webp',
'tank/bullet_sand.webp',
'explosion/explosion1.webp',
'explosion/explosion2.webp',
'explosion/explosion3.webp',
'explosion/explosion4.webp',
'explosion/explosion5.webp',
]);
}
ok,資源加載完畢,開(kāi)始代碼部分。
以玩家坦克為例我們先要繼承一下baseComponent,同時(shí)我們需要分別控制身體和炮塔,所以需要分別進(jìn)行繪制,即兩個(gè)Sprite。
class Tank extends BaseComponent{
final TankGame game;
Sprite bodySprite,turretSprite;
//坦克出生位置
Offset position;
Tank(this.game,{this.position}){
//炮塔
turretSprite = Sprite('tank/t_turret_blue.webp');
//坦克身體
bodySprite= Sprite('tank/t_body_blue.webp');
}
//調(diào)整坦克整體大小的系數(shù)
final double ratio = 0.7;
@override
void render(Canvas canvas){
drawBody(Canvas canvas);
}
@override
void update(double t){}
}
我們?cè)趓ender方法中添加一個(gè)drawBody()方法,來(lái)繪制坦克 :
void drawBody(Canvas canvas){
//對(duì)畫(huà)布操作前要先保存一下
canvas.save();
canvas.translate(position.dx, position.dy);
//繪制tank身體
bodySprite.renderRect(canvas,Rect.fromLTWH(-20*ratio, -15*ratio, 38*ratio, 32*ratio));
// 繪制炮塔
turretSprite.renderRect(canvas, Rect.fromLTWH(-1, -2*ratio, 22*ratio, 6*ratio));
canvas.restore();
}
坦克大小我是直接寫(xiě)的數(shù)值,而后面的ratio,是我用來(lái)調(diào)整大小用的。
現(xiàn)在我們的‘不會(huì)動(dòng)’坦克就繪制完成了。
后面我們需要將搖桿和坦克聯(lián)系起來(lái)已達(dá)到控制坦克的目的,不過(guò)礙于篇幅(我現(xiàn)在滑動(dòng)頁(yè)面都已經(jīng)卡頓了)且控制坦克這三個(gè)方法需要詳盡的說(shuō)一下,因此我將挪到下一篇再講,謝謝大家閱讀。
再次感謝官方的文檔及其貢獻(xiàn)者,給我提供了很大的幫助,如果你很著急可以直接查閱官方文檔