Links to the previous chapter
1, the preface
Life is a game, in this game, we will meet all kinds of hardships. The game is also life, constantly overcome each suffering is the fun of the game, but this game has no suffering, so, when a creator, add a little suffering to the game!
2. Game difficulty design
In infinite parkour games, it’s common to get faster and faster as you play longer and shorter distances between obstacles. But these speeds and distances have to be limited, otherwise it’s a death-trap that’s not very friendly to the player’s experience.
In this game, speed and obstacle distance are also the starting point. To make it easier to control these parameters, I added a configuration to the game that says
The initial velocity, the maximum velocity, the acceleration at each time and the minimum distance of the obstacle
This is also convenient for later debugging, to find a suitable speed and distance.
Dart class GameConfig{static double minSpeed = 6.5; // lib/config.dart class GameConfig{static double minSpeed = 6.5; Static double maxSpeed = 13.0; Static double acceleration = 0.001; static double obstacleMinDistance = 281; }...Copy the code
Add a current speed parameter to the Game class and set this parameter to the minimum speed when the game starts.
// lib/game.dart class MyGame... double currentSpeed; // With TapDetector class, you can give the entire game canvas a click event, click on the game canvas to start voidonTap() {if(! isPlay){ isPlay =true; currentSpeed = GameConfig.minSpeed; }}Copy the code
Overwrite the update method to speed up the current speed every frame until it reaches the maximum speed
@override
void update(double t) {
if(size == null)return;
if(isPlay){
if(currentSpeed <= GameConfig.maxSpeed){ currentSpeed += GameConfig.acceleration; }}}Copy the code
The game class has been changed so that each component of the game also needs to update the screen at the current speed, but the component update method does not have the speed parameter, so this method is no longer needed.
You can customize a method in the component class to receive the t parameter of the previous update, as well as the current speed.
Chestnut (Horizon ground class) :
// lib/sprite/horizon.dart class Horizon... @override void update(double t) {} void updateWithSpeed(double t, double speed){ double x = t * 50 * speed; . Previous update code}Copy the code
Then, in the game update method, call the component’s Update WithSpeed and pass in the current speed
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; }}}Copy the code
3. Add obstacles
As usual, measure the location of the obstacle in the image and write it in config.dart
ObstacleConfig
Once you’re done, create an obstacle component class in the lib/ Sprite directory in the obstacle. Dart file.
class Obstacle... . voidclear() { components.clear(); lastComponent = null; } void updateWithSpeed(double t, double speed) { double x = t * 50 * speed; // Release off-screenfor (final c in components) {
final component = c as SpriteComponent;
if (component.x + component.width < 0) {
components.remove(component);
continue; } component.x -= x; } // Add obstaclesif(lastComponent = = null | | (lastComponent. X-ray lastComponent. Width) < size. Width) {/ / the difficulty of the game is divided into three final double difficulty = (GameConfig.maxSpeed - GameConfig.minSpeed) / 3; speed = speed - GameConfig.minSpeed; double distance; int obstacleIndex; // Randomly create obstaclesif(speed <= difficulty) {// Minimum difficultyif (Random().nextInt(2) == 0) return; // 1/2 chance not to create obstacleIndex = 2; // Random distance between the minimum barrier distance and 3 screen widths = getRandomNum(GameConfig. ObstacleMinDistance, size.width * 3);
} else if(speed <= difficulty * 2) {// Normal difficultyif (Random().nextInt(20) == 0) return; // 1/20 chance not to create obstacleIndex = 3; // Random distance between the minimum barrier distance and 2 screen widths = getRandomNum(GameConfig. ObstacleMinDistance, size.width * 2);
} else{/ / the hardestif (Random().nextInt(60) == 0) return; // 1/60 chance not to create obstacleIndex = 5; // Random distance between the minimum barrier distance and 1 screen width = 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) {// Create an 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);returncomponent; }...Copy the code
4. Collision detection
In this game, all sprites can act as a rectangle
You can make it easier in code to just check if they don’t overlap. So we can figure out the X-axis
Role on the right side of the left side of the < = obstacles | | obstruction on the right side of the < = character on the left
And let’s figure out the Y-axis
Role at the bottom of the head of obstacles of < = | | obstacles at the bottom of the < = character’s head
In flutter, we don’t have to deal with any of this. Flutter provides a Rect class to represent a rectangle, and it provides the overlaps method to detect overlaps. All we need to do is convert a component to a Rect instance.
final Rect rect1 = com1.toRect(); final Rect rect2 = com2.toRect(); Rect2. Overlaps (rect1) / / returntrueIt means collisionCopy the code
If you run it now, you will find that the experience is terrible. The game is gameOver even though you haven’t touched it.
There are many ways to solve this problem, the simplest is to make a judgment based on pixels, first intercept the image where they intersect
Convert to 8-bit byteList, in this list, as long as they have the same location in color they collide, in other words, greater than zero.
Ps: the 8-bit image has no transparency, or the transparent areas are black. If you have a black part of the image to calculate collisions, you can convert it to 32 bits and judge “(val >> 24) > 0”.
5. Add collision methods
Let’s create a collision helper class (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 the edge is touched, check whether the pixel is touchedifRect2.left, rect2.left), Max (rect1.top, rect2.left, rect2.left) 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++) {// the colorless pixels are 0if (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 {
returnnull; } // Open canvas recorder final UI.picturerecOrder Recorder = UI.picturerecOrder (); Canvas canvas = Canvas(recorder); drawImageRect(sprite.image, rect.fromltwh (sprite.src.right - (comds.right-dst.left), sprite.src.bottom - (comDst.bottom - dst.top), dst.width, dst.height), Rect.fromLTWH( 0, 0, dst.width, dst.height, ), Paint()); 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();
returnbyteData.buffer.asUint8List(); }}Copy the code
Then give Obstacle a way to detect collisions
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; } // check only the first two i++;if (i >= 2) break;
}
return false;
}
Copy the code
Finally, the game class invokes the collision method of Obstacle to detect collisions
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; }}}Copy the code
If you want to visually view the collision area, you can add two image components to display in the callback method
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),
),
));
}
Copy the code
Packaging operation
Six, the concluding
The whole game is like this, there are many details not perfect, too lazy to write. Code written is not very good, interested friends can download the source code to run to play.
- Yards cloud: gitee.com/lowbibibi/f…