TankCombat series of articles

If you don’t know about Flame, check it out here:

A look at Flutter’s performance in game development and its cross-platform advantages

Flutter&Flame — TankCombat game development (part 1)

Flutter&Flame — TankCombat game development part 2

Flutter&Flame — TankCombat game development part 3

Flutter&Flame — TankCombat game development (4)

rendering

Pretty good, let me add it to give you an overall impression of what you are doing 🙂

starts

In this chapter, we start making designs for firing shells and enemy tanks

fire

Remember this code?

Row(children: [SizedBox(width: 48)), FireButton(onTap: tankGame.onFireButtonTap, ), Spacer(), FireButton( onTap: tankGame.onFireButtonTap, ), SizedBox(width: 48), ], ),Copy the code

In the runApp method in the main function, these are our fire buttons. As you can see, the click event triggers the onFireButtonTap method in the game. Let’s see how it works:

void onFireButtonTap(){ if(blueBulletNum < 20){ bullets.add(Bullet(this,BulletColor.BLUE,tank.tankId ,position: tank.getBulletOffset(),angle: tank.getBulletAngle())); }}Copy the code

Add a bullet to the bullets(list) and pass the location of the tank and the game object to the bullet. The other two parameters are left out. Let’s start with the class bullet

Bullet

First let’s make bullet integrate with baseComponent and create some variables. You can abstract bullets into tanks so they are essentially indistinguishable and even simpler

As follows:

class Bullet extends BaseComponent{ final TankGame game; final double speed; // Bullet speed Offset position; // Double Angle = 0; // Bullet Angle bool isOffScreen = false; Final Sprite blueSprite = Sprite('tank/bullet_blue.webp'), // Whether to hit bool isHit = false; }Copy the code

Now that some of the bullet’s basic properties are declared, we can manipulate the bullet in the Render and Update methods. Look at the update:

@override void update(double t) {// If (isHit) return; if(isOffScreen)return; Position = position + offset.fromdirection (Angle,speed * t); position = position + Offset (Angle,speed * t); IsOffScreen if (position.dx < -50) {isOffScreen = true; } if (position.dx > game.screenSize.width + 50) { isOffScreen = true; } if (position.dy < -50) { isOffScreen = true; } if (position.dy > game.screenSize.height + 50) { isOffScreen = true; }}Copy the code

Look at the render:

@override void render(Canvas Canvas) {// If (isHit) return; // If (isHit) return; if(isOffScreen)return; canvas.save(); Canvas. Translate (position.dx, position.dy); canvas.rotate(angle); BlueSprite. RenderRect (canvas, rect. fromLTWH(-4, -2, 8, 4)); canvas.restore(); }Copy the code

Ok, that’s it, and now we’re back in the game

TankGame

All components need to be associated with the game, otherwise there is no way to update and render the screen (only the game).

Since we’re sure there’s more than one bullet, let’s create a list

List<Bullet> bullets; / / shellsCopy the code

It is then instantiated in resize

@override void resize(Size size) { screenSize = size; //initEnemyTank(); if(bg == null){ bg = BattleBackground(this); } if(tank == null){ tank = Tank( this,position: Offset(screenSize.width/2,screenSize.height/2), ); } if(bullets == null){ bullets = List(); }}Copy the code

We then pass it the key parameter t in update and call the bullet’s update method

@override void update(double t) {bullets. ForEach ((element) {// bullets element.update(t); } / / remove bullets fly out of the screen. The removeWhere ((element) = > element. The isHit | | element. IsOffScreen); }Copy the code

We call the render method of the bullet in the Render method and pass the canvas to it

@override
  void render(Canvas canvas) {
  	 bg.render(canvas);
    //tank
    tank.render(canvas);
    //bullet
    bullets.forEach((element) {
      element.render(canvas);
    });
  }
Copy the code

Now that we have completed the function of the tank firing shells, let’s go through the general process:

The above images can also help you understand how Component/Sprite works in game.

Right now we can fire shells, but we can’t hit people. In other words, we need to add some enemies first.

The enemy tank TankModel

There are many functions that can be shared between enemy tanks and player tanks. Let’s first abstract a TankModel for enemy tanks:

abstract class TankModel{ final int id; final TankGame game; Sprite bodySprite,turretSprite; // Offset position; TankModel(this.game,this.bodySprite,this.turretSprite,this.position): id = DateTime.now().millisecondsSinceEpoch+Random().nextInt(100); Final int seedNum = 50; final int seedRatio = 2; Double movedDis = 0; // Final double speed = 80; // Final double turnSpeed = 40; // double bodyAngle = 0; // Double turretAngle = 0; // Double targetBodyAngle; // Double targetTurretAngle; //tank alive bool isDead = false; // Move to target position Offset targetOffset; Final double ration = 0.7; // Obtain the bulletoffset (); /// double getBulletAngle(); }Copy the code

It’s a bunch of properties. There’s nothing to talk about. Now we are building tanks based on the model, here we will build a green enemy tank

GreenTank

First we inherit tankModel and then mix in baseComponent as follows:

Class GreenTank extends TankModel with BaseComponent{// Tank bodyRect; // Tank barrel Rect turretRect; GreenTank(TankGame game, Sprite bodySprite, Sprite turretSprite,Offset position) : super(game, bodySprite, turretSprite,position){ bodyRect = Rect.fromLTWH(-20*ration, -15*ration, 38*ration, 32*ration); turretRect = Rect.fromLTWH(-1, -2*ration, 22*ration, 6*ration); generateTargetOffset(); } void generateTargetOffset(){ double x = Random().nextDouble() * (game.screenSize.width - (seedNum * seedRatio)); double y = Random().nextDouble() * (game.screenSize.height - (seedNum * seedRatio)); targetOffset = Offset(x,y); Offset temp = targetOffset - position; targetBodyAngle = temp.direction; targetTurretAngle = temp.direction; } @override void render(Canvas canvas) { if(isDead) return; drawBody(canvas); } @override void update(double t) { rotateBody(t); rotateTurret(t); moveTank(t); }}Copy the code

The constructor initializes some basic properties, which we’ve already covered and won’t repeat. Let’s look at this extra method

  void generateTargetOffset(){
    double x = Random().nextDouble() * (game.screenSize.width - (seedNum * seedRatio));
    double y = Random().nextDouble() * (game.screenSize.height - (seedNum * seedRatio));

    targetOffset = Offset(x,y);

    Offset temp = targetOffset - position;
    targetBodyAngle = temp.direction;
    targetTurretAngle = temp.direction;

  }
Copy the code

This method is used to generate a random target point for the tank to drive through, and from the target point we save the target Angle (turret and hull).

The render and update functions are basically the same as before, except for moveTank (t), which looks like this:

void moveTank(double t) { if(targetBodyAngle ! = null){ if(targetOffset ! MovedDis += speed * t; If (movedDis < 100){if(bodyAngle == targetBodyAngle){//tank position = position + Offset.fromDirection(bodyAngle,speed*t); Position = position + Offset. FromDirection (bodyAngle,turnSpeed*t); }}else{// We recalculate the new target point movedDis = 0; generateTargetOffset(); }}}}Copy the code

After the above movement, our enemy tanks will no longer “head to the south wall”, but walk a distance will turn itself, more flexible and vivid.

Ok, the enemy tanks are complete, we are starting to combine them with game

Combination of start

TankGame

We added two lists to the game to manage enemy tanks of two colors

  List<GreenTank> gTanks = [];
  List<SandTank> sTanks = [];
Copy the code

We then initialize four enemy tanks in the tankGame constructor initialization

TankGame(){ observer = GameObserver(this); initEnemyTank(); } void initEnemyTank() {var turretSprite = Sprite('tank/t_turret_green.webp'); var bodySprite= Sprite('tank/t_body_green.webp'); GTanks. Add (GreenTank (this, bodySprite turretSprite, Offset (100100))); GTanks. Add (GreenTank (this, bodySprite turretSprite, Offset (100, screenSize. Height * 0.8))); ///sand var turretSpriteS = Sprite('tank/t_turret_sand.webp'); var bodySpriteS = Sprite('tank/t_body_sand.webp'); STanks. Add (SandTank (this, bodySpriteS turretSpriteS, Offset (screenSize. Width - 100100))); STanks. Add (SandTank (this, bodySpriteS turretSpriteS, Offset (screenSize. Width - 100, screenSize. Height * 0.8))); }Copy the code

Now we have two tanks ready to go in our warehouses gTanks and sTanks. Now start them!

In the update and render methods we add the following code:

update

gTanks.forEach((element) { element.update(t); }); sTanks.forEach((element) { element.update(t); }); // Remove death tank gTanks. RemoveWhere ((element) => element.isdead); sTanks.removeWhere((element) => element.isDead);Copy the code

render

    gTanks.forEach((element) {
      element.render(canvas);
    });
    sTanks.forEach((element) {
      element.render(canvas);
    });
Copy the code

Functions have been mentioned above.

Now when you run it, you can see 4 enemy tanks running all over the map, but they don’t fire yet, let’s add this feature.

Computer firing function

First of all, we consider that the blue, green and yellow tank shells are different, and other functions may be added later. In order to distinguish them, we will add an enumeration to the bullet class file:

enum BulletColor{
  BLUE,GREEN,SAND
}
Copy the code

Then we added a way for enemy tanks to fire in the game:

  void enemyTankFire<T extends TankModel>(BulletColor color,T tankModel){
    bullets.add(Bullet(this,color,tankModel.id
        ,position: tankModel.getBulletOffset(),angle: tankModel.getBulletAngle()));
  }
Copy the code

The principle is the same as the player tank firing. To avoid too many shells getting stuck, we increased the ammo limit for the enemy

Add two variables to game

Int sandBulletNum = 0; Int blueBulletNum = 0;Copy the code

The game update method counts the number of shells on screen

    blueBulletNum = 0;
    greenBulletNum = 0;
    sandBulletNum = 0;
    bullets.forEach((element) {
      switch(element.bulletColor){

        case BulletColor.BLUE:
          blueBulletNum ++;
          break;
        case BulletColor.GREEN:
          greenBulletNum ++;
          break;
        case BulletColor.SAND:
          sandBulletNum ++;
          break;
      }
      element.update(t);
    });
Copy the code

We then added two lines of code to the enemy tank greenTank update method:

@override void update(double t) { rotateBody(t); rotateTurret(t); moveTank(t); If (game.greenbulletnum < 10){game.enemytankfire (bulletcolor.green, this); }}Copy the code

Now when we run it, we’ll see the enemy’s small tanks running all over the place, turning their turrets and firing!

Ok, we’re almost done. In the next chapter we’ll add the ability to destroy tanks with shells and explosions, as well as the GameObserver design.

Thanks for reading, give a like if you like :)Copy the code

DEMO

Tanks war