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里面

写好了之后,在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轴
角色的底部 <= 障碍物的头部 || 障碍物的底部 <= 角色的头部

在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、结语
整个游戏就这样了,还有很多细节没完善,懒得写。 代码写的也不是很好,感兴趣的朋友可以下载源码来运行玩一下。