Tutorial: Snooker

How to make a simple Snooker game with Cocos Creator

Note: This tutorial is written for Cocos Creator v2.0.10. You can click here to get it and install it.

Before Starting

Before starting to build games with Cocos Creator, you should have a familiar understanding with Cocos Creator. If you have any questions, or need a quick refresher, please review Cocos’s official documentation: Physics System, UI, Listening to and launching events, Actions and Action Lists, Prefab and Atlas Asset.

Let’s start

Use the physics system

We use physical simulation to achieve the movement of the balls, so that the balls can move as realistically as possible. The physics system is closed by default in Cocos Creator, but we can start to use it with this code:

let physicsManager = cc.director.getPhysicsManager(); 
physicsManager.enabled = true;

Also, we can set the physics system to debug mode so that we can see more debug info in preview:

cc.director.getPhysicsManager().debugDrawFlags = cc.PhysicsManager.DrawBits.e_aabbBit |
    cc.PhysicsManager.DrawBits.e_pairBit |
    cc.PhysicsManager.DrawBits.e_centerOfMassBit |
    cc.PhysicsManager.DrawBits.e_jointBit |
    cc.PhysicsManager.DrawBits.e_shapeBit
    ;

You can copy this code in your script to open the physics system, but usually it is suggested that developers create a physics manager component to implement setting features. Example:

/**
 * physics manager component script
 */
 
cc.Class({
    extends: cc.Component,

    properties: {
       active: { 
           default: true,
           tooltip: 'open physics system on component enabled',
       },
       aabb:{
           default: true,
           tooltip: 'drawing AABB rect debug info',
       },
       pair: {
           default: true,
       },
       centerOfMass: { 
           default: true,
           tooltip: 'drawing collider's center of mass'
       },
       joint: { 
           default: true,
           tooltip: 'drawing joint info between colliders'
       },
       shape: {
           default: true,
           tooltip: 'drawing shape info of colliders'
       },
       gravity: {
           default: cc.v2(0,-960),
           tooltip: 'the physics worl gravity, default is -960 y-axis'
       }
    },

    onEnable() {
        //open or close physics system
        let physicsManager = cc.director.getPhysicsManager();
        if (physicsManager.enabled && this.active) {
            cc.warn('The physical system is enabled!');
        }
        physicsManager.enabled = this.active;

        if (!this.active) {
            return;
        }
        //set gravity value
        physicsManager.gravity = this.gravity;

        //set physics debug flag info
        let drawBits = cc.PhysicsManager.DrawBits;
        if (CC_PREVIEW) {
            physicsManager.debugDrawFlags = 
            (this.aabb && drawBits.e_aabbBit) |
            (this.pair && drawBits.e_pairBit) |
            (this.centerOfMass && drawBits.e_centerOfMassBit) |
            (this.joint && drawBits.e_jointBit) |
            (this.shape && drawBits.e_shapeBit);
        } else {
            physicsManager.debugDrawFlags = 0;
        }  
    },
    
    onDisable() {
          //close physics system when this component is disabled
        let physicsManager = cc.director.getPhysicsManager();
        physicsManager.debugDrawFlags = 0;
        physicsManager.enabled = false;
    }
});

When you’ve completed this script, you can add it to Canvas node, Then you can see its Property Settings Panel.

Example: make sure that you have set gravity to 0,0 and open the physics system. For a good preview, select the Shape option.

Why do we set gravity to 0,0? Because our game is a top view game, if we use the default gravity of 0,-960, then the balls will fall down to the bottom of the screen, influenced by gravity. We do not want this behavior, so we use 0,0.

Create the table node

Create a Sprite node named table. Its parent is the Canvas node. This node is our billiard table. Set its SpriteFrame property using the name bg.

Now we implement its physics features:

  1. Since the table is motionless, we set the Type property with Static.
  2. Unselect the AwakeOnLoad property, because we do not want it awakened.

  1. Make sure to select the option Enable Contact listener.
  2. Add a 4 four-box collider component, through these collisions. The balls can not be dropped outside the table.

  1. You can select the option Editing, and then you can change the collider size or position at scene editor.
  2. The box colliders tag property should be 0.
  3. Add six circle collider components. Through these collisions, we can know when the ball goes into the bag.
  4. Set these circle collider tag property to 1. This property will help us to know which collider is colliding .

When you finish these steps, you will see the table in the scene editor with debug info drawing. Example:

Red balls

  1. First, create a Sprite node named rball and give the ball_red.png texture to its spriteframe property.
  2. The ball also needs a circle collider to simulate a real hit. To do this we add it and adjust the size to fit with the ball.
  3. Select the Bullet property of the collider that we added. This is because the ball sometimes will have a burst in movement., If we did not select Bullet, the ball would cross other balls that also had a burst movement.
  4. Unselect Enabled Contact Listener, we do not need to handle any callback on the red balls.
  5. Set Type property with Dynamic, because the balls are movable.
  6. Select Allow Sleep, we need sleep status to help us judge when we could go to the next round and hit more balls. If all the balls are asleep, we can move on in the game; otherwise we can not handle the cue.
  7. Set Linear Damping to 1.2, and set Angular Damping to 0.8, though those properties we can let the balls slow down and sleep.

Ok, when those steps above are finished, a red ball is implemented. We can copy it and paste to create more red balls, also we can adjust those balls’ position, like this:

Next, we can make those red ball nodes, the child node, of an empty node named RedBalls. After editing the RedBalls Node in the scene, we drag it directly from Node Tree to Assets, thus, we create a prefab. Though this prefab, we can dynamically create red balls conveniently.

Cue ball

The cue ball is the same as a red ball. Only the spriteFrame property is different, used ball_white, it’s named wball

Cue

Create an empty node named cue as a child node of the wball node (the cue-ball node), and create a sprite node, as a child node, of this empty node that we just created.
Set the spriteframe property with cue asset. Adjust position like:

The hierarchical relationship of the nodes is designed for to easily calculations of the cue angle. Also, we make cue-ball and cue node as a prefab, and then we can dynamically create it conveniently.

when you do that steps all above, your node tree should be like this:

Logic Scripts

Next, we will develop the scripts for our game logic.

Cue Script “Cue.js”

This script component will be added to the Cue node. As we know, the cue needs to implement the following feature:

  • Listen to mouse events and control the direction of the cue through the movement of the mouse.
  • Listen the to the press event of the left mouse button to control the strength of the cue by the pressing time.
  • When the left mouse button is pressed, the cue moves backward, when the left mouse button is released, the cue moves forward and strike the cue-ball
  • When the cue strikes the cue-ball, a custom event is sent to the cue-ball and makes it move to a new location.
  • Listen to the custom event sent by the cue-ball, so that during the cue-ball movement, the cue cannot operate and hide from the display

After defining the requirements, we can start coding now:

The first thing to do is register some event listener in the start method:

start () {
    //register mouse movement event
    cc.Canvas.instance.node.on(cc.Node.EventType.MOUSE_MOVE, this.onMouseMove, this);
    //register pressing event of left mouse button
    cc.Canvas.instance.node.on(cc.Node.EventType.MOUSE_DOWN, this.onMouseDown, this);
    //register uppend event of left mouse button
    cc.Canvas.instance.node.on(cc.Node.EventType.MOUSE_UP, this.onMouseUp, this);
    //register the custom event sent by cue-ball when the cue-ball is stop
    cc.Canvas.instance.node.on("wball-sleep", this.onwballSleep, this);
},

After registering the events, we set a property of type cc.Node named cue,this property refer to the child node of cue,this is, the node that displays cue texture.

properties: {
        //the sprite node of cue
        cue : cc.Node
    },

next, we implement callbacks of those events one by one:
the callback of mouse move event

onMouseMove (event) {
    //When the mouse is pressed or the cue is not displayed,
    //the cue does not move any more. Invalid operation for cue
    if (this._mouseDown || this.node.opacity != 255) {
        return;
    }
    //Get the current position of the mouse
    var loc = event.getLocation();
    this._mousePosition = loc;
    //Converting the mouse position to parent coordinates
    loc = this.node.parent.convertToNodeSpaceAR(loc);
    //Calculate the pin with (-1,0) vector, which is the angle of the cue that need to rotate
    var angle = loc.signAngle(cc.v2(-1,0));
    angle = cc.misc.radiansToDegrees(angle);
    //set the rotation of the cue
    this.node.rotation = angle;
},

the callback of left mouse button pressing event:

onMouseDown (event) {
    //Invalid operation for cue when it is not displayed
    if (this.node.opacity != 255) {
        return;
    }
    //set the mouse down flag to true
    this._mouseDown = true;

    //Move the cue backward by 50 pixels per second
    //Here you can let the value of - 50 to component properties then we can easy to debug
    this.cue.runAction(cc.repeatForever(cc.moveBy(1, cc.v2(-50, 0))));
},

the callback of left mouse button upped event

onMouseUp (event) {
    if (this.node.opacity != 255) {
        return;
    }
    //Calculate the pixels of the cue moving backward,
    //and use this value to calculate the strength of the striking
    var force = this.cue.x - 182;
    //Stop the moveing backward action
    this.cue.stopAllActions();
    //use sequence action
    this.cue.runAction(cc.sequence(
        //here (-182,0) is the original position of the cue
        //you can let the value of -182 to component properties then we can easy to debug
        cc.moveTo(0.1, cc.v2(-182,0)).easing(cc.easeSineOut()),
        cc.callFunc(() => {
            //set the mouse down flag to false
            this._mouseDown = false;
            //create a custom event 'cue', and dispatch it
            //we designed two params for the custom event
            //the first param is the force value though it we can calculate the strike strength
            //the second param is the mouse position through it we can calculate the strike angle 
            var customEvent = new cc.Event.EventCustom("cue", true);
            customEvent.force = force;
            customEvent.cue = this._mousePosition
            this.node.dispatchEvent(customEvent);
            //hide the cue
            this.node.opacity = 0;
        })
    ));
},

the custom event sent by cue-ball when the cue-ball is stopped

onballSleep () {
    //display the cue when the cue-ball has stopped
    this.node.opacity = 255;
},

Then we finished the coding of Cue.js script.

Cue-ball Script “wball.js”

The requirement of the cue-ball is simple, send the cue ball is all we need to do:

  • Listen to the custom event ‘cue’ that is sent by cue.js and use it to calculate the velocity of the cue-ball after it has been hit.
  • When the cue-ball has stopped, that is, when the awake property of the cue-ball rigidbody is false, then send a custom event named wball-sleep to the cue . The cue could know that it can be displayed and operated.

Let’s start coding!

First, is register event listeners in the start method

    start () {
        //listen the custom event 'cue'
        this.node.on("cue", this.onCue, this);
        //this flag is used for sign whether the cue-ball has stopped
        //it mainly used to send custom events only once during cue-ball is quiet
        //the cue-ball is start with sleep status, so we set the defualt value is true
        this._sleep = true;
    },

next is implement the callback when we received the cue event

    onCue (event) {
        if (this && this.node.parent) {
            //cue-ball start to move,so this flag turn to false
            this._sleep = false;
            //calcuate the direction of cue-ball movement
            var direction = this.node.parent.convertToNodeSpaceAR(event.cue);
            direction = direction.sub(this.node.position);
            direction = direction.normalize();
            //calcuate the linear velocity of the cue-ball and apply it according to the direction and force
            this.node.getComponent(cc.RigidBody).linearVelocity = direction.mul(-Math.pow(1.016, Math.abs(event.force)));
        }
    },

Finally is send wball-sleep event when the cue-ball is stopped

    update (dt) {
        if (!this.node.getComponent(cc.RigidBody).awake && !this._sleep) {
            //when the movement of cue-ball had stopped, we turn this flag to true,
            //and send the custom event 'wball-sleep' to tell the cue that the cue-ball had stopped
            this._sleep = true;
            cc.Canvas.instance.node.emit("wball-sleep");
        }
    },

Table Script “table.js”

This script is used to for the table. It helps us to implement these features:

The logic of ball-in-bag:

  • Scored when we let the red ball into the bag, and decision og winning or losing.
  • Reset the position of the cue-ball when it comes into the bag.
  • When we start or restart the game, reset all the balls including the cue-ball and red balls, and reset the score.

The most important code is to implement the logic of bagging. This part of the code requires us to implement the onBeginContact method. onBeginContact callback is invoked when a collision is detected. If the script, which belongs to the same node with the collider component that implemented it.

Note: The Collider Components in the same node, will not do collision detecsion detection with each other forever.
Note: We need to select Enable contact listener in the rigidbody component properties, so that a corresponding callback will be generated, that is, we did it before.

This callback has 3 params: contact, selfCollider, otherCollider. It is easy to understand. As the name suggests, selfCollider refers to the collider on the node of the callback script, otherCollider refers to the other collider. The most important information is included in contact, like the location of the collision, the normal vector. Let’s implement this callback:

    onBeginContact(contact, selfCollider, otherCollider) {
        //if the tag property of the selfCollider is 1,
        //that means a collision occurs between a ball and the bag
        if (selfCollider.tag === 1) {
            //if the ball is cue-ball
            if (otherCollider.node.name === "wball") {
                //emit a custom event for reset cue-ball
                this.node.emit("wball", otherCollider.node);
            }
            else {
                //if the ball is red ball
                //the numbers of remaining red balls reduced by 1
                this._ballNum--;
                //update the label display
                this.ballLabel.string = "Remaining" + this._ballNum + "ball";
                //when the remaining red balls is 0, we win the game
                if (this._ballNum <= 0) {
                    this.gameUI.active = false;
                    this.winUI.active = true;
                }
            }
            //remove the node of ball from node tree
            otherCollider.node.removeFromParent(true);
        }
    },

Next, is the logic of reset cue-ball and red balls. It is simple. At this point, we will use the prefab of cue-ball and red ball that we made before:

    resetTable () {
        this.node.removeAllChildren(true);
        this.resetWhiteBall();
        this.resetRedBalls();
        this._ballNum = 10;
        this.winUI.active = false;
        this.startUI.active = false;
        this.endUI.active = false;
        this.gameUI.active = true;
        this.ballLabel.string = "Remaining" + this._ballNum + "ball";
    },
    
    resetWhiteBall () {
        var node = cc.instantiate(this.wballPrefab);
        node.parent = this.node;
    },

    resetRedBalls () {
        var node = cc.instantiate(this.redBalls);
        node.parent = this.node;
    },

Now, when we invoke resetTable function, we can reset the game easily. We can also easily reset cue-ball and red ball by calling resetWhiteBall and resetRedBalls.
Don’t forget register the custom event wball that we used in onBeginContact callback:

    start () {
        this.node.on("wball", (node) => {
            setTimeout(() => {
                this.resetWhiteBall();
            }, 100);
        })
    },

Ultimate

At this point, our billiard game had completed the core part, leaving behind some logic on UI, it is simple, but doesn’t implement every step. I hope that through the production of this mini-game can help you to understand Cocos Creator and its physics system
Here is the url of the completed code and project of the game on GitHub

6 Likes

More tutorials to come! Thank you everyone!

I’m still using CocoStudio and I love snooker… so I think it’s time to start with Cocos Creator following this tutorial, thanks!

1 Like

This is really good start. Really looking forward for next tutorials.
Try to cover different topics.

1 Like

Thank you for the feedback.

Nice, but Chinese characters are hurting my eyes :smiley:

Oh, well, learning Chinese would solve this :slight_smile: I’ll see what I can change, if it is text.

3 Likes

Thank you for this tutorial. I have followed it and think I must have done something wrong, because when I try to run it I get an error:

Simulator: E/jswrapper (272): ERROR: Uncaught TypeError: Cannot read property ‘awake’ of null, location: src/cocos2d-jsb.js:0:0

STACK:
[0]update@assets/wball.js:46
[1]anonymous@(no filename):3
[2]anonymous@src/cocos2d-jsb.js:15355
[3]invoke@src/cocos2d-jsb.js:15338
[4]updatePhase@src/cocos2d-jsb.js:15456
[5]mainLoop@src/cocos2d-jsb.js:9587
[6]callback@src/cocos2d-jsb.js:9827
[7]tick@jsb-adapter/jsb-builtin.js:2006
E/jswrapper (562): [ERROR] (c:\fb\cxp_220\cocos\scripting\js-bindings\jswrapper\v8\object.cpp, 562): Invoking function (099C2280) failed!
at HTMLElement.print-simulator-log (C:\CocosCreator_2.2.2\resources\app.asar\editor\builtin\scene\panel\messages\scene.js:1:1608)
at Object.e._dispatch (C:\CocosCreator_2.2.2\resources\app.asar\editor-framework\lib\renderer\panel.js:1:1876)
at EventEmitter. (C:\CocosCreator_2.2.2\resources\app.asar\editor-framework\lib\renderer\ipc.js:1:2952)
at EventEmitter.emit (events.js:194:13)

I don’t know if it helps, but the line of code in wball.js it seems to be referring to is:

if (!this.node.getComponent(cc.RigidBody).awake && !this._sleep) {

If I understand the error message correctly, it seems to be saying that this.node.getComponent(cc.RigidBody) is returning ‘null’. I have checked that my cue ball (wball) node has a rigid body component attached to it, so I don’t know why it is null.

I wonder if anyone has any ideas where I’m going wrong? Thanks! :slight_smile:

We can ask @jare to help. I haven’t tried to build this tutorial in I think 8-9 months.

What version of Cocos Creator are you using?

Thanks slackmoehrle, this was using 2.2.2.

Appreciate the quality tutorial :slight_smile:
But it feels like a carrom game rather than snooker( the way balls interact with physics) .
I think 3d is essential for the realistic snooker feel!
Or can you think of ways to imitate those in 2d?? Curious.