1、前言
人生是一場遊戲,在這場遊戲中,我們會遇到各種磨難。遊戲也是人生,不斷戰勝每個磨難才是遊戲的樂趣,但是這個遊戲還沒有磨難,所以,當個創世主,給遊戲新增一點磨難吧!
2、遊戲難度設計
像這種無限的跑酷遊戲,普遍的做法是玩的時間越長速度越來越快、障礙物的距離越來越短。但是這些速度和距離都要有個限制,不然的話,就會出現必死結局,對玩家的體驗很不友好。
在本遊戲中,也是從速度和障礙物距離下手。為了方便控制這些引數,我給遊戲新增了一個配置,配置裡面寫上
開始時的速度、最大的速度、每次的加速度和障礙物最小的距離
這樣也方便後面除錯的時候,找到一個合適的速度和距離。
// lib/config.dart
class GameConfig{
static double minSpeed = 6.5;
static double maxSpeed = 13.0;
static double acceleration = 0.001;
static double obstacleMinDistance = 281;
}
...
複製程式碼
再給game類新增一個當前速度引數,在遊戲開始的時候,把這個引數設定為最小的速度。
// lib/game.dart
class MyGame...
double currentSpeed;
//with TapDetector這個類,可以給整個遊戲畫布一個點選事件,點選遊戲畫布開始
void onTap(){
if(!isPlay){
isPlay = true;
currentSpeed = GameConfig.minSpeed;
}
}
複製程式碼
重寫update方法,每一幀的時候給當前的速度加速,到最大速度就不加了
@override
void update(double t) {
if(size == null)return;
if(isPlay){
if(currentSpeed <= GameConfig.maxSpeed){
currentSpeed += GameConfig.acceleration;
}
}
}
複製程式碼
game類改了,遊戲每個元件也需要隨著當前速度更新畫面,但是元件的update方法沒有速度這個引數,所以不需要這個方法了。
可以在元件類自定義一個方法,讓它接收之前update的t引數,和當前的速度。
栗子(Horizon地面類):
// lib/sprite/horizon.dart
class Horizon...
@override
void update(double t) {}
void updateWithSpeed(double t, double speed){
double x = t * 50 * speed;
...之前update的程式碼
}
複製程式碼
然後在game的update方法中,呼叫元件的updateWithSpeed,把當前速度傳進去
class MyGame...
@override
void update(double t) {
if(size == null)return;
if(isPlay){
horizon.updateWithSpeed(t, currentSpeed);
cloud.updateWithSpeed(t, currentSpeed);
obstacle.updateWithSpeed(t, currentSpeed);
dino.updateWithSpeed(t, currentSpeed);
if(currentSpeed <= GameConfig.maxSpeed){
currentSpeed += GameConfig.acceleration;
}
}
}
複製程式碼
3、新增障礙物
老規矩,先測量障礙物在圖片中的位置,把它寫進config.dart裡面
配置類的程式碼就不放了,類名叫ObstacleConfig寫好了之後,在lib/sprite目錄下建立obstacle.dart檔案,裡面寫障礙物元件類。
class Obstacle...
...
void clear() {
components.clear();
lastComponent = null;
}
void updateWithSpeed(double t, double speed) {
double x = t * 50 * speed;
//釋放超出螢幕的
for (final c in components) {
final component = c as SpriteComponent;
if (component.x + component.width < 0) {
components.remove(component);
continue;
}
component.x -= x;
}
//新增障礙
if (lastComponent == null ||
(lastComponent.x - lastComponent.width) < size.width) {
//把遊戲分成3個難度
final double difficulty = (GameConfig.maxSpeed - GameConfig.minSpeed) / 3;
speed = speed - GameConfig.minSpeed;
double distance;
int obstacleIndex; //隨機建立障礙物
if (speed <= difficulty) {
//最小難度
if (Random().nextInt(2) == 0) return; // 1/2機率不建立
obstacleIndex = 2; //2種型別障礙物隨機建立
//障礙物距離在最小障礙物距離到3個螢幕寬度之間隨機
distance = getRandomNum(GameConfig.obstacleMinDistance, size.width * 3);
} else if (speed <= difficulty * 2) {
//普通難度
if (Random().nextInt(20) == 0) return; // 1/20機率不建立
obstacleIndex = 3;
//障礙物距離在最小障礙物的距離到2個螢幕寬度之間隨機
distance = getRandomNum(GameConfig.obstacleMinDistance, size.width * 2);
} else {
// 最難
if (Random().nextInt(60) == 0) return; // 1/60機率不建立
obstacleIndex = 5;
//障礙物距離在最小障礙物的距離到1個螢幕寬度之間隨機
distance = getRandomNum(GameConfig.obstacleMinDistance, size.width * 1);
}
double x = (lastComponent != null
? (lastComponent.x + lastComponent.width)
: size.width) +
distance;
lastComponent = createComponent(x, obstacleIndex);
add(lastComponent);
}
}
SpriteComponent createComponent(double x, int obstacleIndex) {
//隨機建立障礙物
final int index = Random().nextInt(obstacleIndex);
final Sprite sprite = Sprite.fromImage(spriteImage,
width: ObstacleConfig.list[index].w,
height: ObstacleConfig.list[index].h,
y: ObstacleConfig.list[index].y,
x: ObstacleConfig.list[index].x);
SpriteComponent component = SpriteComponent.fromSprite(
ObstacleConfig.list[index].w, ObstacleConfig.list[index].h, sprite);
component.x = x + ObstacleConfig.list[index].w;
component.y =
size.height - (HorizonConfig.h + ObstacleConfig.list[index].h - 22);
return component;
}
...
複製程式碼
4、碰撞檢測
在這個遊戲中,所有的精靈都可以當作一個矩形
如果兩個矩形重疊,就代表它們碰撞了,在檢測的時候,數學上可以處理成比較中心點的座標在x和y方向上的距離和寬度的關係。在程式碼中可以簡單點,只要判斷他們不重疊的情況就可以了。可以先判斷x軸
角色的右邊 <= 障礙物的左邊 || 障礙物的右邊 <= 角色的左邊
再判斷y軸
除了這些情況,其它的都是重疊了。角色的底部 <= 障礙物的頭部 || 障礙物的底部 <= 角色的頭部
在flutter中,這些其實都不用我們處理,flutter提供了一個Rect類來表示一個矩形,它提供了overlaps方法來檢測重疊,我們要做的只是把一個元件轉為Rect例項就可以了。
final Rect rect1 = com1.toRect();
final Rect rect2 = com2.toRect();
rect2.overlaps(rect1) //返回true代表碰撞了
複製程式碼
是不是覺得很簡單,如果現在就執行的話,你會發現體驗很差,明明都沒有碰到,遊戲卻gameOver了。
看圖,把它們當成矩形來做判斷的話,碰撞的時候,透明區域也算碰上了。要解決這個問題的方法有很多,最簡單的就是基於畫素來做判斷,先擷取他們相交地方的影像
轉為8位的byteList,在這個list中,只要他們相同的位置有顏色就代表他們碰撞了,換句話來說,就是大於0。
ps:8位的影像沒有透明度,或者說透明的地方也是黑色。如果你的圖片有黑色的部分要計算碰撞的話,可以轉為32位,判斷“ (val >> 24) > 0 ”。
5、新增碰撞方法
先建立一個碰撞幫助類(HitHelp)
typedef DebugCallBack = void Function(ui.Image img1, ui.Image img2);
class HitHelp {
static checkHit(PositionComponent com1, PositionComponent com2,
[DebugCallBack debugCallBack]) async {
final Rect rect1 = com1.toRect();
final Rect rect2 = com2.toRect();
//邊碰到了, 判斷畫素是否碰到
if (rect2.overlaps(rect1)) {
//相交的矩形
final Rect dst = Rect.fromLTRB(
max(rect1.left, rect2.left),
max(rect1.top, rect2.top),
min(rect1.right, rect2.right),
min(rect1.bottom, rect2.bottom));
final ui.Image img1 = await getImg(com1, dst, rect1);
final ui.Image img2 = await getImg(com2, dst, rect2);
if (debugCallBack != null) {
debugCallBack(img1, img2);
}
List<int> list1 = await imageToByteList(img1);
List<int> list2 = await imageToByteList(img2);
for (int i = 0; i < list1.length; i++) {
//無色的畫素點是0
if (list1[i] > 0 && list2[i] > 0) {
return true;
}
}
}
return false;
}
static Future<ui.Image> getImg(
PositionComponent component, Rect dst, Rect comDst) async {
Sprite sprite;
if (component is SpriteComponent) {
sprite = component.sprite;
} else if (component is AnimationComponent) {
sprite = component.animation.getSprite();
} else {
return null;
}
//開啟畫布記錄儀
final ui.PictureRecorder recorder = ui.PictureRecorder();
Canvas canvas = Canvas(recorder);
//根據元件的相交位置繪製圖片
canvas.drawImageRect(
sprite.image,
Rect.fromLTWH(
sprite.src.right - (comDst.right - dst.left),
sprite.src.bottom - (comDst.bottom - dst.top),
dst.width,
dst.height),
Rect.fromLTWH(
0,
0,
dst.width,
dst.height,
),
Paint());
//關閉記錄
final ui.Picture picture = recorder.endRecording();
return picture.toImage(dst.width.ceil(), dst.height.ceil());
}
static Future<Uint8List> imageToByteList(ui.Image img) async {
ByteData byteData = await img.toByteData();
return byteData.buffer.asUint8List();
}
}
複製程式碼
然後給Obstacle一個檢測碰撞的方法
class Obstacle...
...
Future<bool> hitTest(PositionComponent com1, DebugCallBack debugHit) async {
int i = 0;
for (final SpriteComponent com2 in components) {
if (await HitHelp.checkHit(com1, com2, debugHit)) {
return true;
}
//只檢查最前面的兩個
i++;
if (i >= 2) break;
}
return false;
}
複製程式碼
最後game類呼叫Obstacle的碰撞方法檢測碰撞
class MyGame...
...
@override
void update(double t) async {
if(size == null)return;
if(isPlay){
...
if(await obstacle.hitTest(dino.actualDino, this.debugHit)){
dino.die();
isPlay = false;
}
}
}
複製程式碼
如果想要直觀的檢視碰撞區域的話,可以在回撥方法中新增兩個image元件顯示
void debugHit(ui.Image img1, ui.Image img2){
addWidgetOverlay('a1', Positioned(
right: 100,
top: 0,
child: Container(
width: 100,
height: 100,
color: Colors.blueGrey,
child: RawImage(image: img1,fit: BoxFit.fill),
),
));
addWidgetOverlay('a2', Positioned(
right: 0,
top: 0,
child: Container(
width: 100,
height: 100,
color: Colors.brown,
child: RawImage(image: img2,fit: BoxFit.fill),
),
));
}
複製程式碼
打包執行
6、結語
整個遊戲就這樣了,還有很多細節沒完善,懶得寫。 程式碼寫的也不是很好,感興趣的朋友可以下載原始碼來執行玩一下。