這篇文章是接着上一章寫的,若是沒有看過上一章,能夠經過查看公衆號"bugporter"的歷史記錄獲取上一章的內容,或者經過如下連接查看。bash
上一章全部須要用到屏幕尺寸的 組件(Component)類都是在resize方法中接收到包含屏幕尺寸的Size參數後才構建的。可是每一個類都這樣寫,有點不友好,因此我把構造方法改了一下,讓它直接接收Size參數,而後在MyGame類的resize方法中,把接收到Size參數給到組件後再實例化這些組件。ide
以前的地面(Horizon)組件類示例:函數
lib/sprite/horizon.dartoop
class Horizon ...{
...
Horizon(this.spriteImage);
@override
void resize(ui.Size size) {
super.resize(size);
if(components.isEmpty){
init();
return;
}
}
...
}
複製代碼
更改後post
class Horizon ...{
...
ui.Size size;
Horizon(this.spriteImage, this.size){
init();
}
//再也不須要重寫resize了
複製代碼
其它組件類也這樣改,而後咱們在MyGame類的resize中,才實例化這些組件優化
lib/game.dart動畫
Class MyGame...
@override
void resize(ui.Size size) {
if(components.isEmpty){
gameBg = GameBg(Color.fromRGBO(245, 243, 245, 1));
horizon = Horizon(spriteImage, size);
cloud = Cloud(spriteImage, size);
obstacle = Obstacle(spriteImage, size);
}
super.resize(size);
}
...
複製代碼
在上一章已經完成了遊戲背景、地面、和天空(雲朵),如今來建立遊戲最重要的一部分,遊戲主角,那個會跳不會rap也不會籃球的 小恐龍(dino)。ui
除了跳小恐龍還會什麼?this
這裏面有兩個狀態我解釋一下:
等待: 遊戲未開始時小恐龍的樣子,開始後它須要跑到屏幕的必定距離,咱們才能控制它
驚訝: 這圖像中的小恐龍很驚訝,由於它碰到障礙物,Game Over了!
知道這些狀態後,須要測量出這些狀態對應的圖像位置和大小,而後把它寫到配置中。
lib/config.dart
...
class DinoConfig{
static double h = 94.0;
static double y = 2.0;
}
class DinoJumpConfig{
static double w = 88.0;
static double x = 1336.5;
}
class DinoWaitConfig{
static double w = 88.0;
static double x = 1336.5+88;
}
class DinoRunConfig{
static double w = 88;
final double x;
const DinoRunConfig._internal({this.x});
static List<DinoRunConfig> list = [
DinoRunConfig._internal(
x: 1336.5+(88*2)
),
DinoRunConfig._internal(
x: 1336.5+(88*3)
),
];
}
class DinoDieConfig{
static double w = 88;
static double x = 1336.5+(88*4);
}
class DinoDownConfig{
static double w = 118;
final double x;
const DinoDownConfig._internal({this.x});
static List<DinoDownConfig> list = [
DinoDownConfig._internal(
x: 1866.0
),
DinoDownConfig._internal(
x: 1866.0+118
),
];
}
複製代碼
上面代碼中,我爲小恐龍每一個狀態的圖像位置都建立了一個配置類。在這些配置中,它們的h(高)和y軸有些不是同樣的,因此我把它放到DinoConfig中,把這些狀態的高和y軸都強制同樣,能夠方便控制它的y軸實現跳躍。否則的話,須要計算每一個狀態的跳躍高度,還有站在地面上的高度。
裏面的蹲和站兩個跑步狀態是由多個圖像組成的動畫,因此我爲它們寫了一個私有的構造方法,並經過一個靜態的List返回每一個圖像不一樣的地方。
爲何要這樣返回呢?是由於在flame這個框架中,它爲咱們提供了一個動畫Animation類來建立動畫,咱們能夠經過它的spriteList構造方法來建立。在這個方法中,須要一個Sprite類型的List,因此咱們能夠經過遍歷配置中的List,把建立的Sprite對象加入到動畫組件的List中。
栗子
List<Sprite> runSpriteList = [];
DinoRunConfig.list.forEach((DinoRunConfig config){
runSpriteList.add(Sprite.fromImage(spriteImage,
x: config.x,
y: DinoConfig.y,
width: DinoRunConfig.w,
height: DinoConfig.h),
);
});
//AnimationComponent 動畫組件,須要3個參數,寬、高和動畫對象。
//stepTime每幀的時間,loop是否循環播放
AnimationComponent(
DinoRunConfig.w,
DinoConfig.h,
Animation.spriteList(runSpriteList, stepTime: 0.1, loop: true));
複製代碼
這裏面有個地方須要注意一下,若是在父組件中把這個動畫組件添加進去了,可是重寫了父的update方法時,還須要在父的update中調用動畫組件的update方法,這個動畫纔會播放。
配置寫好了,如今來建立主角的組件。打開lib/script目錄,在這個目錄下建立一個dino.dart
在dino.dart中,先建立一個枚舉,把小恐龍在整個遊戲中的狀態寫上
enum DinoStatus {
waiting,
running,
jumping,
downing,
die,
}
複製代碼
五個狀態,分別是:等待中、跑步中、跳躍中、正在蹲着和game over了
建立好了以後,在枚舉代碼的下邊,咱們建立一個組件類dino。在這個類中定義一個list屬性,並把上面枚舉對應狀態的組件都添加進去,最後還須要一個status屬性來記錄小恐龍當前的狀態。
enum DinoStatus...
class Dino extends Component{
List<PositionComponent> actualDinoList = List(5);
DinoStatus status = DinoStatus.waiting; //默認是等待中
Dino(ui.Image spriteImage, this.size) {
final double height = DinoConfig.h;
final double yPos = DinoConfig.y;
//建立枚舉對應的組件,加進list屬性
//waiting
actualDinoList[0] = SpriteComponent.fromSprite(
DinoWaitConfig.w,
height,
Sprite.fromImage(spriteImage,
x: DinoWaitConfig.x,
y: yPos,
width: DinoWaitConfig.w,
height: height));
//running
List<Sprite> runSpriteList = [];
DinoRunConfig.list.forEach((DinoRunConfig config){
runSpriteList.add(Sprite.fromImage(spriteImage,
x: config.x,
y: yPos,
width: DinoRunConfig.w,
height: height),
);
});
actualDinoList[1] = AnimationComponent(
DinoRunConfig.w,
height,
Animation.spriteList(runSpriteList,
stepTime: 0.1,
loop: true));
//jumping
actualDinoList[2] = SpriteComponent.fromSprite(
DinoJumpConfig.w,
height,
Sprite.fromImage(spriteImage,
x: DinoJumpConfig.x,
y: yPos,
width: DinoJumpConfig.w,
height: height));
//downing
List<Sprite> downSpriteList = [];
DinoDownConfig.list.forEach((DinoDownConfig config){
downSpriteList.add(Sprite.fromImage(spriteImage,
x: config.x,
y: yPos,
width: DinoDownConfig.w,
height: height),
);
});
actualDinoList[3] = AnimationComponent(
DinoDownConfig.w,
height,
Animation.spriteList(downSpriteList,
stepTime: 0.1,
loop: true));
//die
actualDinoList[4] = SpriteComponent.fromSprite(
DinoDieConfig.w,
height,
Sprite.fromImage(spriteImage,
x: DinoDieConfig.x,
y: yPos,
width: DinoDieConfig.w,
height: height));
}
}
複製代碼
狀態對應的組件加到list了,咱們還須要根據當前的狀態來渲染不一樣的組件。
首先在類中定義一個獲取器,返回當前的狀態對應的組件
Dino(ui.Image spriteImage, this.size)...
//獲取當前狀態對應的組件
PositionComponent get actualDino => actualDinoList[status.index];
複製代碼
而後重寫render方法,把當前狀態的組件渲染出來
...
@override
void render(ui.Canvas c) {
actualDino.render(c);
}
...
複製代碼
如今,小恐龍組件已經被建立好了,咱們回到MyGame這個類中,把它添加進去
class MyGame...
...
Dino dino;
@override
void resize(ui.Size size) {
if(components.isEmpty){
...
dino = Dino(spriteImage, size);
this
..add(gameBg)..add(horizon)..add(cloud)..add(dino)
...
複製代碼
ps: ... 是省略以前的代碼的意思
打包運行:
恐龍飛起來了,是由於在添加時,還沒給它設置y軸的位置,因此默認是0的。如今咱們給它添加一個y軸的位置,屏幕高-(地面高+恐龍高-再站下一點點的距離)
class Dino...
...
double maxY;
double x,y;
Dino(ui.Image spriteImage, this.size) {
final double height = DinoConfig.h;
final double yPos = DinoConfig.y;
maxY = size.height - (HorizonConfig.h + height - 22);
x = 0;
y = maxY;
//waiting
actualDinoList[0] = SpriteComponent.fromSprite(
DinoWaitConfig.w,
height,
Sprite.fromImage(spriteImage,
x: DinoWaitConfig.x,
y: yPos,
width: DinoWaitConfig.w,
height: height))
..x=x..y=y;
... 其餘組件也這樣設置一下x和y。
}
複製代碼
上面代碼的maxY: 地面的位置,也就是恐龍最大的y軸位置。
dino類不須要添加子組件,由於它每次都是根據狀態來渲染一個組件的,只是起到了調度的做用,因此沒有繼承PositionComponent,而是繼承了基礎的Component類。這樣作的話,須要給它一個x和y屬性,咱們在渲染子組件的時候,把子組件的x和y設置成dino類的,能夠方便外面控制或者獲取,後面進行破撞檢測的時候會用到。
如今再運行:
打開main.dart文件,調用runApp方法時,是獲取了Game的widget屬性做爲參數給runApp方法的。既然Game類返回了widget,那麼咱們也能夠把它放到flutter的其它組件中,例如給它套一個Stack, 把遊戲返回的widget放在底下,把一些按鈕添加到遊戲的上面,而後經過按鈕的點擊事件,實現對遊戲的控制。
可是想偷懶,不想寫一堆flutter的widget怎麼辦?
在fleam0.18.0以上的版本,提供了一個HasWidgetsOverlay類,只要咱們在Game類中with了這個類,就能夠使用addWidgetOverlay方法,把一個widget添加到遊戲的上面了,它底層就是使用Stack封裝的。
打開game.dart文件,給MyGame類添加一個建立按鈕的方法
...
class MyGame...
Widget createButton({@required IconData icon, double right=0, double
bottom=0,
ValueChanged<bool>
onHighlightChanged}){
return Positioned(
right: right,
bottom: bottom,
child: MaterialButton(
onHighlightChanged: onHighlightChanged,
onPressed: (){},
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
child: Container(
width: 50,
height: 50,
decoration: new BoxDecoration(
color: Color.fromRGBO(0, 0, 0, 0.5),
//設置四周圓角 角度
borderRadius: BorderRadius.all(Radius.circular(50)),
//設置四周邊框
border: new Border.all(width: 2, color: Colors.black),
),
child: Icon(icon, color: Colors.black,),
),
),
);
}
...
複製代碼
該方法接收一個按鈕長按事件的回調函數onHighlightChanged,要想按鈕監聽長按事件,必需要給按鈕一個點擊事件onPressed,因此我在按鈕的onPressed中寫了一個空的回調函數。
爲何不直接用點擊事件呢?
由於點擊事件是在手指離開屏幕以後才觸發的,會有一點延遲,因此用長按事件,能夠監聽到玩家按下和鬆開,在這裏我須要它按下後就立刻跳,還有蹲下須要一直按住按鈕。
onHighlightChanged每次點擊都會觸發兩次,在按下和鬆開按鈕的時候觸發,回調中接收了一個bool類型的參數,按下是true、鬆開是false
而後咱們在MyGame的resize方法中,建立跳和蹲的按鈕,而後調用addWidgetOverlay添加到遊戲的上面
void resize(ui.Size size) {
...
this
..add(gameBg)..add(horizon)..add(cloud)..add(dino)..add(obstacle)
..addWidgetOverlay('upButton', createButton(
icon: Icons.arrow_drop_up,
right: 50,
bottom: 120,
onHighlightChanged: (isOn)=>dino?.jump(isOn),
))
..addWidgetOverlay('downButton', createButton(
icon: Icons.arrow_drop_down,
right: 50,
bottom: 50,
onHighlightChanged: (isOn)=>dino?.down(isOn),
));
...
複製代碼
在onHighlightChanged中調用dino類的jump和down方法,這兩個方法尚未,咱們須要在dino類中實現它。
class Dino...
...
bool isJump = false;
bool isDown = false;
double jumpVelocity = 0.0;
...
void jump(bool isOn) {
if(status == DinoStatus.running && isOn){
status = DinoStatus.jumping;
this.jumpVelocity = jumpPos;
isJump = true;
return;
}
isJump = false;
}
void down(bool isOn){
isDown = isOn;
if(status == DinoStatus.running && isOn){
status = DinoStatus.downing;
return;
}
if(status == DinoStatus.downing && !isOn){
status = DinoStatus.running;
return;
}
}
@override
void update(double t) {
if (status == DinoStatus.jumping) {
y += jumpVelocity;
jumpVelocity += gravity;
if(y > maxY){
status = DinoStatus.running;
y = maxY;
//一直按住,不斷跳
jump(isJump);
//跳的過程當中按了蹲,角色落地時蹲下
down(isDown);
}
}
actualDino..x=x..y=y;
actualDino.update(t);
}
複製代碼
跳躍的時候給了它一個瞬間向上的力,而後不斷給它一個重力讓它回到地面。只有跑的時候能跳或者蹲,若是是跳,回到地面後還按着跳沒鬆開那麼將繼續跳,蹲的時候按下立刻蹲,鬆開了就站着跑。
把默認狀態改成runing, 運行後..
錄成gif看着有點卡,其實是很流暢的..