After playing with synthetic watermelon these two days, I became enthusiastic and thought I could write one. I’ve never written a game before, but of course as a programmer with unlimited 3 minutes of heat I’ve swiped Cocos Creator. So toss over a, from the entry to the life of the first small game. On the whole:

Full source code and preview

Github.com/yieio/daxig…

Preview the address

To collect material

Synthesis watermelon is an H5 mini game made by Biwing Interactive using Cocos Creator V2.2.2. This is a very good team, every week will produce a casual game, if you look at their previous games, you will find that the synthesis of large watermelon is done step by step, before this game they made a similar game called ball ball match, with different elements and almost the same gameplay. In order to respect the work of others, I hereby declare that the game materials collected in this paper belong to the team and are only used for learning and practice.

Open Chrome games g.vsane.com/game/552/, F…

Include all Assets by XHR Requests = Include all Assets by XHR Requests = Include all Assets by XHR Requests = Include all Assets by XHR Requests = Include all Assets by XHR Requests = Include all Assets by XHR Requests Then Save All Resources.

All the images and sound materials are in the RES directory. The SRC directory contains the program’s logic, project.js.

Install CocosCreator

Download from CocosCreator. This article uses v2.4.3. The DashBoard downloaded here is like a version and project manager for CocosCreator. After you download and install it, you need to download and install the corresponding version of the CocosCreator editor.

If you’re like me, you’ve never worked with CocosCreator before, I strongly recommend you get started with this quick tutorial: Make your first game and go through it. Familiarize yourself with the features and concepts involved in Cocos Creator.

Building a Game interface

With the material, we started building the game interface. The interface was very simple: a background image, a bottom image, and then a score number. The location of the fruit will be discussed later.

Create a new project using the Helloworld-typescript project template. Familiarize yourself with typescript. This is a wise choice. TypeScript is friendly in all of these ways. It also works very well.

PNG and *. Mp3 files are copied into the assets directory of the project.

Set the background image and the bottom table image

  1. Select the Canvas node, set Design Resolution to 720×1280 in the property inspector on the right, remove Fit Height and select Fit Width. Select Width Fit because the game we make needs to align the screen with the left and right edges, Height is not that important. Of course, the back will also say how to adapt to the height of the problem.
  2. Select the Canvas right button to create an empty node and change the name to bgLayer in the property inspector.
  3. Drag the background image from Explorer under this node and select the name wall.
  4. Drag the bottom table image under the bgLayer node and rename it Down. Set the following properties:

Here, the original height was 127px and the height was 500px. The type of SLICED was selected so that the background would not leak out at the bottom when the height was suitable. Select the SLICED type, we need to specify the stretch area of the image, otherwise the whole image will be stretched, click edit next to the Sprite Frame, as shown below, and click the green tick when you are done.

At this point, the background and bottom of the main section of the interface are set up. But if you look at it directly, you’ll see that under the iPhoneX simulator there’s a black edge at the top. Here we use code directly to make the background image full screen:

  1. Right-click in assets Script of the Resource manager, create a TypeScript Script FillScreen, and open it with the [VsCode] editor as follows:
@ccclass
export default class FillScreen extends cc.Component {

  onLoad() {
    this.node.setContentSize(cc.winSize.width, cc.winSize.height);
  }

  start(){}}Copy the code
  1. Select the Wall node in The Hierarchy Manager, in the Properties Inspector, Add Components -> User Script Components, and select the script you just wrote. This will fill the background image in full screen.

Set score panel

The score palette is placed in the upper left corner of the screen, using the Label(text) and creating a scoreLabel node under the Canvas. Font is art font, the creation method is to use the following digital image to create a new art digital atlas. Right-click New -> Art Digital Configuration in Explorer, then click on the new Art Digital Configuration, look at the Properties Inspector and drag the following image into the RawTextureFile input box – don’t make it a selected image as you drag it, otherwise the selection will change.

Set the following values:

Once you’ve made the font, click on the scoreLabel node and set the [Properties Inspector], font is the font we just made.

The score panel is in place, but a preview shows that the distance from the top on the iPhoneX is not what we want

This is the same relative positioning problem we had in the previous step when we used the program to change the background to fill the black edge. We also need to automatically adjust the distance of the score panel from the top through the script.

AdjustWithHeight (AdjustWithHeight) AdjustWithHeight (AdjustWithHeight)

export default class AdjustWithHeight extends cc.Component {

       @property
    offset:number = 0;

    // Whether to display the entry animation
    @property
    hasShowEffect:boolean = false;

    onLoad () {
        let start = 0;
        start = cc.winSize.height / 2;
        this.node.y = start;
        if(!this.hasShowEffect){
            this.node.y += this.offset;
        }
    }  

    start () {
        this.showTheNode(); 
    }

    showTheNode(){ 
        if(this.hasShowEffect){
            // Slide down from the top
            this.node.runAction(cc.moveTo(. 5, 
            cc.v2(this.node.x, 
            this.node.y + this.offset)).easing(cc.easeBackOut())); }}}Copy the code

Add the script to the scoreLabel node as a component. The script above left an offset parameter, which can be set. Now it is equivalent to placing the node at the top. The reason for not writing offset is that later other nodes will need to use this script to control the distance to the top.

Add the fruit

The scene is almost built, now it is time to make the top of the fruit node, the main function here is the user click a random fruit, and then fall.

AdjustWithHeight = AdjustWithHeight = AdjustWithHeight = AdjustWithHeight = AdjustWithHeight

Fruit node

DashLine is the red line beyond which the game ends

The fruit node here is only for display use at present. When we do the function realization, we need to drag it to the resource manager to make it into a prefabricated resource. The specific production method is described in the official introduction tutorial mentioned above. So once we’ve made it into a pre-made resource we can delete the Fruit node here.

Click to generate a fruit

Create a New Typescript script for MainGame and add it to the Canvas. MainGame controls the logical functions of the game. First, it listens for click /Touch events.

export default class Fruit extends cc.Component {

    // The fruit number is also used to index the fruit spirit image to be displayed
    fruitNumber = 0;
    start () {

    }
}

Copy the code
@ccclass
export default class MainGame extends cc.Component {
  // List of fruit sprites
  @property([cc.SpriteFrame])
  fruitSprites: Array<cc.SpriteFrame> = [];

  // score label Label
  @property(cc.Label)
  scoreLabel: cc.Label = null;

  // Fruit prefabricated node resource
  @property(cc.Prefab)
  fruitPre: cc.Prefab = null;

  // Top area node to which the fruit is to be added
  @property(cc.Node)
  topNode: cc.Node = null;

  // Use to temporarily store the generated fruit nodes
  targetFruit: cc.Node = null;

  // The fruit count has been created
  createFruitCount:number = 0;

  start() {
      this.createOneFruit(0);

      this.bindTouch();
  }

  // Create a fruit
  createOneFruit(index: number) {
    var t = this,
      n = cc.instantiate(this.fruitPre);
    n.parent = this.topNode;
    n.getComponent(cc.Sprite).spriteFrame = this.fruitSprites[index];
    // Get the Fruit script component attached to the Fruit node, note that the name is case-sensitive
    n.getComponent("Fruit").fruitNumber = index; 

    // A new display effect
    n.scale = 0;
    cc.tween(n)
      .to(
        0.5,
        {
          scale: 1}, {easing: "backOut",
        }
      )
      .call(function () {
        t.targetFruit = n;
      })
      .start(); 
  }

  // Bind the Touch event
  bindTouch() {
    this.node.on(cc.Node.EventType.TOUCH_START, this.onTouchStart, this),
      this.node.on(cc.Node.EventType.TOUCH_MOVE, this.onTouchMove, this),
      this.node.on(cc.Node.EventType.TOUCH_END, this.onTouchEnd, this),
      this.node.on(cc.Node.EventType.TOUCH_CANCEL, this.onTouchEnd, this);
  }

  / / touch
  onTouchStart(e: cc.Event.EventTouch) {
    if (null= =this.targetFruit) {
      return;
    }

    // Click on the position to assign the x value to the fruit
    let x = this.node.convertToNodeSpaceAR(e.getLocation()).x,
      y = this.targetFruit.position.y;
    cc.tween(this.targetFruit)
      .to(0.1, {
        position: cc.v3(x, y),
      })
      .start();
  }

  / / drag
  onTouchMove(e: cc.Event.EventTouch) {
    if (null= =this.targetFruit) {
      return;
    }

    this.targetFruit.x = this.node.convertToNodeSpaceAR(e.getLocation()).x;
  }

  / / end of Touch
  onTouchEnd(e: cc.Event.EventTouch) {
      let t = this;
    if (null == t.targetFruit) {
      return;
    }

    // Let the fruit fall
    //todo...
 
    // remove the temporary reference
    t.targetFruit = null;
    // Create a new fruit
    this.scheduleOnce(function () {
      0 == t.createFruitCount
        ? (t.createOneFruit(0), t.createFruitCount++)
        : 1 == t.createFruitCount
        ? (t.createOneFruit(0), t.createFruitCount++)
        : 2 == t.createFruitCount
        ? (t.createOneFruit(1), t.createFruitCount++)
        : 3 == t.createFruitCount
        ? (t.createOneFruit(2), t.createFruitCount++)
        : 4 == t.createFruitCount
        ? (t.createOneFruit(2), t.createFruitCount++)
        : 5 == t.createFruitCount
        ? (t.createOneFruit(3), t.createFruitCount++)
        : t.createFruitCount > 5 &&
          (t.createOneFruit(Math.floor(Math.random() * 5)),
          t.createFruitCount++);
    }, 0.5); }}Copy the code

FruitSprites attribute stores all fruit pictures. In order to use the pictures more conveniently, the pictures are renamed and numbered by 0-10. What is needed can be obtained directly from the github address mentioned above. The @property parameter defined by the script can be set in the [property inspector]. Resources and node classes can be dragged and dropped directly as follows:

This completes the interface click to generate a new fruit function.

Fruit landing

To make the fruit fall, we need to use the physical system of the CoCOS engine, and add the RigidBody and PhysicsCircleCollider to the fruit. These are in the component -> physical component, set the gravity multiple, friction force and elastic force, etc., properties are as follows:

With that set up, let’s directly turn on the physical system’s gravity in the onLoad() event callback of MainGame to see what happens.


 onLoad() { 
      this.physicsSystemCtrl(true.false); 
  }

  physicsSystemCtrl(enablePhysics: boolean, enableDebug: boolean) {
    cc.director.getPhysicsManager().enabled = enablePhysics;
    cc.director.getPhysicsManager().gravity = cc.v2(0, -300);
    if(enableDebug){
        cc.director.getPhysicsManager().debugDrawFlags =
        cc.PhysicsManager.DrawBits.e_shapeBit
    }
    cc.director.getCollisionManager().enabled = enablePhysics;
    cc.director.getCollisionManager().enabledDebugDraw = enableDebug;
  }
Copy the code

As you can see, the fruit drops as soon as it is created. We made some changes to the creation method. After creation, it is not affected by gravity


// Create a fruit
  createOneFruit(index: number) {
    var t = this,
      n = cc.instantiate(this.fruitPre);
    n.parent = this.topNode;
    n.getComponent(cc.Sprite).spriteFrame = this.fruitSprites[index];
    // Get the Fruit script component attached to the Fruit node, note that the name is case-sensitive
    n.getComponent("Fruit").fruitNumber = index; 

    // The radius of the collision physical boundary is 0
    n.getComponent(cc.RigidBody).type = cc.RigidBodyType.Static;
    n.getComponent(cc.PhysicsCircleCollider).radius = 0;
    n.getComponent(cc.PhysicsCircleCollider).apply();

    // omit the following code...
     
  }
Copy the code

  onTouchEnd(e: cc.Event.EventTouch) {
      let t = this;
    if (null == t.targetFruit) {
      return;
    }

    // Let the fruit fall
    let h = t.targetFruit.height;
    t.targetFruit.getComponent(cc.PhysicsCircleCollider).radius = h / 2;
    t.targetFruit.getComponent(cc.PhysicsCircleCollider).apply();
    t.targetFruit.getComponent(cc.RigidBody).type = cc.RigidBodyType.Dynamic;
    t.targetFruit.getComponent(cc.RigidBody).linearVelocity = cc.v2(0, -800);
    
    // omit the following code...
}
Copy the code

Set interface boundaries to catch fruit

Add rigid bodies and corresponding physical collision boundaries to the bottom and left and right nodes of the bglayer.

Final effect:

We hang the fallen fruit on the empty node of a fruitNode under the Canvas, so as to facilitate traversal. It should be noted here that the initial coordinates of fruitNode nodes should be the same as those of topNode. In this way, the coordinates will not be confused when the fruit is transferred from the topNode node to the fruitNode. Because the coordinates in Cocos Creator are relative to the parent node.

Fruit collision detection

In the Property inspector for the Fruit prefab resource we’ve checked Enabled Contact Listener

We can listen to the collision event in his Fruit script, because there will be more Fruit at that time. In order to facilitate collision detection, we can add groups to the nodes with collisions to distinguish detection, and also remove unnecessary collision detection. Fruit goes to fruit, left and right borders go to wall, and bottom tables go to Downwall

We are ready to write the handling of collision events:

//MainGame.ts

// Score changes and results
  scoreObj = {
    isScoreChanged: false.target: 0.change: 0.score: 0};// Set a static singleton reference so that methods of that class can be called from other classes
  static Instance: MainGame = null;

  onLoad() {
    null! = MainGame.Instance && MainGame.Instance.destroy(); MainGame.Instance =this;

    this.physicsSystemCtrl(true.false);
  }
  
  update(dt){
    this.updateScoreLabel(dt);
  }
  
  /** * Create an upgraded fruit *@param FruitNumber Number Indicates the number of the fruit *@param Position cc.vec3 Fruit */
  createLevelUpFruit = function (fruitNumber: number, position: cc.Vec3) {
    let _t = this;
    let o = cc.instantiate(this.fruitPre);
    o.parent = _t.fruitNode;
    o.getComponent(cc.Sprite).spriteFrame = _t.fruitSprites[fruitNumber];
    o.getComponent("Fruit").fruitNumber = fruitNumber;
    o.position = position;
    o.scale = 0; 

    o.getComponent(cc.RigidBody).linearVelocity = cc.v2(0, -100);
    o.getComponent(cc.PhysicsCircleCollider).radius = o.height / 2;
    o.getComponent(cc.PhysicsCircleCollider).apply();
    cc.tween(o)
      .to(
        0.5,
        {
          scale: 1}, {easing: "backOut",
        }
      )
      .call(function () {
        
      })
      .start(); 
  };
  
  //#region scores panel updated

  setScoreTween(score: number) {
    let scoreObj = this.scoreObj; scoreObj.target ! = score && ((scoreObj.target = score), (scoreObj.change =Math.abs(scoreObj.target - scoreObj.score)),
      (scoreObj.isScoreChanged = !0));
  }

  updateScoreLabel(dt) {
    let scoreObj = this.scoreObj;
    if (scoreObj.isScoreChanged) {
      (scoreObj.score += dt * scoreObj.change * 5),
        scoreObj.score >= scoreObj.target &&
          ((scoreObj.score = scoreObj.target), (scoreObj.isScoreChanged = !1));
      var t = Math.floor(scoreObj.score);
      this.scoreLabel.string = t.toString(); }}//#endregion

Copy the code

A collision event in the Fruit.ts script

import MainGame from "./MainGame";


// The number of collisions with the bottom boundary, which is used to mark the sound played when the first collision occurs
downWallColl: number = 0; 
  
// Collision start event
onBeginContact(contact: cc.PhysicsContact, self: cc.PhysicsCollider, other: cc.PhysicsCollider) {
    let _t = this;

    let fruitNode = MainGame.Instance.fruitNode;

    // Whether to collide the bottom boundary
    if (other.node.group == "downwall") {
      // Add it to the fruitNode node after the collision
      self.node.parent = fruitNode;

      // Is it the first collision
      if (_t.downWallColl == 0) {
        // Play collision sound
      }

      _t.downWallColl++;
    }

    // Whether it hit other fruit
    if (other.node.group == "fruit") {
      self.node.parent = fruitNode;

      null! = self.node.getComponent(cc.RigidBody) && (self.node.getComponent(cc.RigidBody).angularVelocity =0);
      // The fruit below hits the fruit above and jumps
      if (self.node.y < other.node.y) {
        return;
      }

      let otherFruitNumber = other.node.getComponent("Fruit").fruitNumber,
        selfFruitNumber = _t.fruitNumber;

      // The two fruits have the same number
      if (otherFruitNumber == selfFruitNumber) {
        // Both are already watermelon cases
        if (selfFruitNumber == 10) {
          return;
        }

        let pos = other.node.position;

        // Combine effects, sounds, scores, and create a new fruit

        / / score
        let score = MainGame.Instance.scoreObj.target + selfFruitNumber + 1;
        MainGame.Instance.setScoreTween(score);

        // Remove collision boundary to avoid collision again
        other.node.getComponent(cc.PhysicsCircleCollider).radius = 0;
        other.node.getComponent(cc.PhysicsCircleCollider).apply();
        self.node.getComponent(cc.PhysicsCircleCollider).radius = 0;
        self.node.getComponent(cc.PhysicsCircleCollider).apply();

        cc.tween(self.node)
          .to(0.1, {
            position: pos, // Synthesize to the position of the impacted fruit
          })
          .call(function () {
            // Create a splash effect

            // Create a synthetic fruit
            MainGame.Instance.createLevelUpFruit(selfFruitNumber + 1, pos);
            // Destroy two colliding fruits
            self.node.active = !1;
            other.node.active = !1; other.node.destroy(); self.node.destroy(); }) .start(); }}}Copy the code

At the beginning of the research, the rigid body type of the prefabricated resource Fruit was set to static, and the new rigid body type generated by the combination was not changed to Dynamic through the program. As a result, when the newly generated Fruit collides with the bottom table and other fruits, the rigid body penetrates. It took me hours to find the problem, but I also wanted to write it down in the hope that other newbies wouldn’t get stuck with the same problem when researching it.

In this case, the score update is to pass the total score and compare it with the original score value, and then update the difference bit by bit into the Score Abel through the update function, forming the score rolling effect.

Static fruit collides with static desktop to cause rigid body penetration:

At this point the fruit is ready to collide and merge to form new fruit.

Fruit hit juice effect

Let’s start by putting the juice effect into the MainGame property.

FruitL is the elongated fruit, GuozhiL is the round fruit juice and GzuozhiZ is the fruit juice

//MainGame.ts
// Fruit granules
  @property([cc.SpriteFrame])
  fruitL: Array<cc.SpriteFrame> = [];
  // The fruit splashes
  @property([cc.SpriteFrame])
  guozhiL: Array<cc.SpriteFrame> = [];
  // Juice effect
  @property([cc.SpriteFrame])
  guozhiZ: Array<cc.SpriteFrame> = [];

  // Juice prefab resource
  @property(cc.Prefab)
  juicePre: cc.Prefab = null;
  // Effect mounts the node
  @property(cc.Node)
  effectNode: cc.Node = null;
  
Copy the code

Special effects code directly borrowed from the original game implementation, removed the node cache function:

createFruitBoomEffect(fruitNumber: number, t: cc.Vec3, width: number) {
    let _t = this;

    for (var o = 0; o < 10; o++) {
      let c = cc.instantiate(_t.juicePre);
      c.parent = _t.effectNode;
      c.getComponent(cc.Sprite).spriteFrame = _t.guozhiL[fruitNumber];
      var a = 359 * Math.random(),
        i = 30 * Math.random() + width / 2,
        l = cc.v2(
          Math.sin((a * Math.PI) / 180) * i,
          Math.cos((a * Math.PI) / 180) * i
        );
      c.scale = 0.5 * Math.random() + width / 100;
      var p = 0.5 * Math.random();
      (c.position = t),
        c.runAction(
          cc.sequence(
            cc.spawn(
              cc.moveBy(p, l),
              cc.scaleTo(p + 0.5.0.3),
              cc.rotateBy(p + 0.5, _t.randomInteger(-360.360))
            ),
            cc.fadeOut(0.1),
            cc.callFunc(function () {
              c.active = !1;
            }, this))); }for (var f = 0; f < 20; f++) {
      let h = cc.instantiate(_t.juicePre);
      h.parent = _t.effectNode;
      (h.getComponent(cc.Sprite).spriteFrame = _t.fruitL[fruitNumber]),
        (h.active = !0);
      (a = 359 * Math.random()),
        (i = 30 * Math.random() + width / 2),
        (l = cc.v2(
          Math.sin((a * Math.PI) / 180) * i,
          Math.cos((a * Math.PI) / 180) * i
        ));
      h.scale = 0.5 * Math.random() + width / 100;
      p = 0.5 * Math.random();
      (h.position = t),
        h.runAction(
          cc.sequence(
            cc.spawn(cc.moveBy(p, l), cc.scaleTo(p + 0.5.0.3)),
            cc.fadeOut(0.1),
            cc.callFunc(function () {
              h.active = !1;
            }, this))); }var m = cc.instantiate(_t.juicePre);
    m.parent = _t.effectNode;
    m.active = true;
    (m.getComponent(cc.Sprite).spriteFrame = _t.guozhiZ[fruitNumber]),
      (m.position = t),
      (m.scale = 0),
      (m.angle = this.randomInteger(0.360)),
      m.runAction(
        cc.sequence(
          cc.spawn(cc.scaleTo(0.2, width / 150), cc.fadeOut(1)),
          cc.callFunc(function () {
            m.active = !1; }))); }Copy the code

Collision sound

Add sound effects to MainGame’s script. There are several sound effects, such as fruit and table collision, fruit combined sound, and a cheering sound after synthesizing a large watermelon. Canvas property inspector, sound resources:

//MainGame. Ts plays the sound effect method, just call the method in the corresponding place

/ * * *@param clipIndex AudioClip The audio clip to play.
     * @param loop Boolean Whether the music loop or not.
     * @param volume Number Volume size.
     */
    playAudio(clipIndex:number, loop:boolean, volume:number) {
        cc.audioEngine.play(this.audios[clipIndex], loop, volume)
    }

Copy the code

To create the effect of a large watermelon

In addition to the above effects, there is also an additional effect of throwing ribbons and a dark screen to show a large watermelon, as follows:

Let’s add a Sprite node to make a prefabricated resource maskBg with the following properties:

Place the ribbon and aperture image into the added Caidai property of the MainGame script, and add an empty daxiguaEffectNode to mount the effect of the composite watermelon.

Game over

Finally, we need to detect whether the fruit has exceeded the red line to determine the end of the game. In the update callback of the Fruit script, we judged whether the Fruit went over the line after it was mounted to the fruitNode node. In addition, we obtained whether the red line distance was nearly reached after each collision detection, and the red line prompt was displayed.

Game end screen

Will not post code, you can directly obtain the source code to view. I don’t have anything special to say.

The last

The game is not complicated, as a beginner reference or quite good. Another area that can be optimized is that some special nodes can be cached and reused, this can be studied. The original game code has a bunch of irrelevant code sandwiched in the middle (or maybe I am not good enough to understand, but it does not affect the game function of the code), so I deliberately recorded my entry into the research process, here, write enough wordy.