I am participating in the nuggets Community game creative submission Contest. For details, please see: Game Creative Submission Contest

preview

The online demo

Web version: V.IDoo. top/mix (please use mobile phone access, not adapted to PC)

Android version: synthesis of large melon. Apk

Introduction to the

Back in the Spring Festival of 2021, a small game named “Synthesis Watermelon” became very popular at that time. It could be seen on the hot list of Weibo every day, and the moments of friends were filled with friends showing off their scores and various naughty operations. It was extremely happy for a time.

Careful analysis, this small game is not complex, you can start to achieve a, eat their own to make the “melon” should be very sweet.

Therefore, during the Spring Festival holiday of 2021, I spent about three days and three nights at home (until the evening of New Year’s Eve ~), and finally made a synthetic watermelon of PLUS version. Let’s call it “synthetic watermelon” for the first time.

Compared to the original synthetic watermelon, our “synthetic watermelon” is also more “human” support the following gameplay:

  1. Support for custom background images (you can change any background you want)
  2. Support to modify the image material (want to change the image of melon into idol’s avatar? Feel free to switch)
  3. Support gravity sensing control (it is said that there is no such thing as a watermelon that cannot be closed, if there is, shake your phone twice more ~)
  4. Support for reverse synthesis of small melons (large melon synthesis of small melons, reverse thinking is, ha ha ha)
  5. Support for generating only small/large melons (every melon falls the same, is there any reason not to make the ultimate melon?)
  6. Built-in multiple game themes (fruit/emoji/school emblem you can choose, I heard emoji and 985 theme of the ultimate boss surprise oh, bad smile.jpg)

See here, everybody to this “melon” still satisfied? Let me take you with the whole of their own “melon” out, ripe!

Implementation details

The rules of the game

As always, here are the rules of the game:

  1. Players tap anywhere on the screen to place the fruit
  2. When two identical fruits come together they form a higher grade of fruit
  3. The result is a large watermelon, and the player wins
  4. The game ends when the fruit exceeds the warning line at the top of the screen

It can be seen that the rules of the game are relatively simple. At present, we need to solve the following parts:

  1. Collision detection between fruits of the same grade
  2. The score rating system and the end of the game
  3. Zoom animations and particle effects for fruit collision upgrades at the same level
  4. Monitor the screen click event to control the fruit fall position

Let’s implement each part together.

Initialize the

Here, we continue to select Flutter + Flame + Forge2D as the main project technology stack.

Flame is a 2D game engine inside the Flutter ecosystem, responsible for rendering the game scene and event loops.

Forge2D, on the other hand, is a physics engine responsible for things like object motion and collision detection in the physical world.

Collision detection

There are two main types of collisions involved in the game: one is the collision between fruit and ground, and the other is the collision between fruit and fruit.

Let’s first define the basic properties of the fruit (sphere) and the ground (wall) :

/// The wall
class Wall extends BodyComponent {
  final Vector2 start;
  final Vector2 end;
  /// The side where metope is, have altogether fluctuation left and right sides 4
  final int side;

  /// A wall is a line from the beginning to the end
  Wall(this.start, this.end, this.side);

  @override
  Body createBody() {
    final shape = PolygonShape();
    shape.setAsEdge(start, end);

    finalfixtureDef = FixtureDef() .. shape = shape .. restitution =0.0
      ..friction = 0.1;

    finalbodyDef = BodyDef() .. userData =this // Detect collisions. position = Vector2.zero() .. type = BodyType.STATIC;// Keep the object stationary

    return world.createBody(bodyDef)..createFixture(fixtureDef);
  }
}

/// Fruit (sphere)
class Ball extends SpriteBodyComponent {
  /// level
  int level;

  /// The radius of
  double _radius;

  /// Falling position
  Vector2 fallPosition;

  /// Whether it's moving horizontally
  bool moving = true;

  /// Whether to start a vertical fall
  bool isFalling;

  /// Whether it landed or collided with other objects
  bool landed = false;

  /// Whether to upgrade
  bool levelUp = false;

  /// Whether to remove
  bool removed = false;

  /// Is expanding into higher grade fruit
  bool bouncing = false; . }Copy the code

Now that we have objects, let’s move on to defining collision detection between objects.

/// The fruit collided with the ground
class BallWallContactCallback extends ContactCallback<Ball.Wall> {
  /// Collision started
  @override
  void begin(Ball ball, Wall wall, Contact contact) {
    if(wall.side ! =3) {
        return; // Non-ground, no response
    }
    if(! ball.landed) {// The fruit falls to the ground
      ball.landed = true;
      // Play the landing soundAudioTool.fall(); }}}/// Fruit colliding with fruit
class BallBallContactCallback extends ContactCallback<Ball.Ball> {
  /// Collision started
  @override
  void begin(Ball ball1, Ball ball2, Contact contact) {
    // The fruit falls to the ground
    ball1.landed = true;
    ball2.landed = true;
    if (ball1.level == ball2.level) {
      // Two fruits of the same grade collide to form a higher grade fruit
      if (ball1.position.y < ball2.position.y) {
        // Remove the fruit below
        ball1.removed = true;
        // Upgrade the fruit above
        ball2.levelUp = true;
      } else {
        ball2.removed = true;
        ball1.levelUp = true; }}}}Copy the code

Particle dynamic effect

A closer look at the game’s graphics shows that there is a dynamic effect of “popping” during fruit synthesis, which we can simulate using Flame’s built-in particle system.

class BloomPartcle {...// Generates an accelerated particle dynamic effect
  AcceleratedParticle generate({
    Offset position,
    Offset angle,
    double speed,
    double radius,
    Color color,
  }) {
    return AcceleratedParticle(
      position: position,
      speed: angle * speed,
      acceleration: angle * radius,
      child: ComputedParticle(
        renderer: (canvas, particle) => canvas.drawCircle(
          Offset.zero,
          particle.progress * 5, Paint() .. color = Color.lerp( color, Colors.white, particle.progress *0.1(), (), (), (); }/// Displays particle kinetic effects at the specified location
  void show(Offset position, double radius) {
    // Randomly generate the number of particles
    final n = randomCount(radius);
    // Randomly generate particle colors
    final color = randomColor(radius);
    gameRef.add(
      ParticleComponent(
        particle: Particle.generate(
          count: n,
          lifespan: randomTime(radius), // The duration of randomly generated particles
          generator: (i) {
            final angle = randomAngle((2 * pi / n) * i); // Randomly generate the particle injection Angle
            return generate(
              position: position,
              angle: Offset(sin(angle), cos(angle)),
              radius: randomRadius(radius), // Randomly generate the particle ejection radius
              speed: randomSpeed(radius), // Randomly generate the particle ejection velocitycolor: color, ); },),),); }}Copy the code

Fruit falling

Fruit falls in two steps: the first step is to move horizontally to the point of fall, and the second step is to fall free.

/// Fruit drop controller
class UpdateBallsFalling extends Component with HasGameRef<MyGame> {
  @override
  void update(double t) {
    // Find and traverse all fruits that have not yet started their free fall
    gameRef.components
        .where((e) => e isBall && ! e.isFalling && b.moving) .forEach((ball) { Ball b = ball;final p = b.position;
      // Expected drop location
      final fp = b.fallPosition;
      // Screen width
      final width = gameRef.viewport.vw(100);
      // The drop position is on the left side of the screen
      final left = fp.x < gameRef.viewport.center.x;
      // The drop position is in the center of the screen
      final center = (fp.x - gameRef.viewport.center.x).abs() < gameRef.viewport.vw(5);
      if(! center && b.body.linearVelocity.x ==0) {
        // The initial speed of the fruit is 0. In the first stage, the fruit moves at a constant speed in the horizontal direction until the target drop point is reached
        b.body.linearVelocity = Vector2((left ? - 1 : 1) * gameRef.viewport.size.velocitySize, 0);
      }
      // The fruit reaches the target drop point, enters the second stage and begins to free fall
      if(center || (left && (p.x < b.radius || p.x < fp.x)) || (! left && (width - p.x < b.radius || p.x > fp.x))) {// Remove the original fruit
        gameRef.remove(ball);
        // Then create a new fruit in place of the old fruit and let it fall
        gameRef.add(Ball.create(gameRef.viewport, position: p, level: b.level, canFall: true)); }}); }}Copy the code

Fruit upgrade

Based on the above analysis, let’s continue to look at the fruit upgrade process:

class UpdateLevelUp extends Component with HasGameRef<MyGame> {
  @override
  void update(double t) {
    // Find the fruit marked for removal and walk through the destruction recycling resources
    gameRef.components
        .where((e) => e is Ball && e.removed)
        .forEach(gameRef.remove);
    // Find the fruit marked as an upgrade and iterate through the upgrade
    gameRef.components.where((e) => e is Ball && e.levelUp).forEach((ball) {
      // Play fruit synthesis sound
      AudioTool.mix();
      Ball b = ball;
      // Update the score
      GameState.updateScore(GameState.score + b.level);
      // Remove old fruit
      gameRef.remove(ball);
      // Get the radius of the new level of fruit
      final radius = Levels.radius(b.level + 1);
      // Replace old fruit with higher level fruitgameRef.add(Ball.create( gameRef.viewport, position: b.position.. y += (radius - b.radius), level: b.level +1,
        canFall: true,
        landed: true,
        bounce: true));// Play composite particle animation
      BloomPartcle(gameRef).show(b.position.toOffset(), radius);
      // Final level (boss)
      final isLastLevel = Levels.isLastLevel(b.level + 1);
      if (isLastLevel) {
        // The highest level of fruit is created, and the player winsGameLife(gameRef).win(); }}); }}Copy the code

The player interaction

There are two interactive modes for players to control fruit fall in “Synthesis melon”, one is to tap the screen, and the other is gravity sensing.

Interestingly, for the second gravity sensing mode, our implementation idea is also based on changing gravity.

One caveat, however, is that the vertical component of the resultant gravity should always be downward to prevent the fruit from rolling back to the top of the screen 🙃

class UpdateGravity extends Component with HasGameRef<MyGame> {
  /// Update gravity in the physical world
  void changeGravity(Vector2 gravity) => gameRef.world.setGravity(gravity);

  /// Screen tilt
  double get tiltDegree {
    final x = SensorTool.xyz.x;
    if (x.abs() > 1) {
      // The screen tilts left and right
      return 2 - * (x / 10);
    }
    // Keep the screen vertical
    return 0;
  }

  @override
  void update(double t) {
    // The magnitude of gravity
    final size = gameRef.size.gravitySize;
    // Ordinary vertical downward gravity
    final gravity = Vector2(0.- 1 * size);
    // Gravity sensor mode is enabled
    if (GameState.gameSetting.gravity) {
      // Gravity offset component caused by mobile phone screen offset
      final offset = Vector2(size, 0) * tiltDegree;
      // Synthesize new gravity (the vertical gravity component is always downward to prevent the fruit from rolling backwards to the top of the screen)
      final newGravity = gravity + offset;
      changeGravity(newGravity);
    } else {
      // The gravity sensor mode is switched off, and the gravity field changes to normal vertical downward gravity
      if(gameRef.world.getGravity() ! = gravity) { changeGravity(gravity); }}}}Copy the code

conclusion

The above is all the implementation points involved in the whole game, I believe that you can also be smart to implement this simple game.

If you are interested in more detailed code implementation, you can go to github.com/idootop/wat… Consult here, refuse white piao, beg a Star 🌟

One more thing

The fun Learning about Flutter “Mini Game” is the last chapter of the “Fun Learning about Flutter” series. It is a fun practice part of Flutter development after you have mastered the basics of Flutter development.

PS: The basic part of “Flutter” series has not been written yet. I will spend some time every week to gradually improve the series. If you are interested in Flutter, please stay tuned.