To make it easier to play the game on mobile, I decided to add two buttons so that people without mouse input can test the game especially on mobile. To do this, right click the canvas->Create->UI Component->Button. Name the button as one
Switch to the 2D view and place the buttons on the bottom left and right sections of the screens. Open each button and click on the labels. Change the String on the Label of each button. The button on the left should be labeled "One" and the right button should be labeled "Two".
Try running the game and make sure that the buttons are visible and well placed as shown in the figure below.
We will now have to create the functions for the buttons to do the same things that the mouse clicks do. Open the PlayerController script and add these two functions as shown.
leftButton(){
//left button clicked
this.jumpByStep(1);
}
rightButton(){
//right button clicked
this.jumpByStep(2);
}
Notice that the code within both functions is copied from the onMouseUp. First with the left button and then the right button. Go back to the editor and click on one button, go to the inspector and increase the number of Click Events to 1. Then drag the Player to the node section and then select the PlayerController and then the function for the appropriate button. For the left button, use the leftButton() function and for the right button choose the rightButton() function. See the screenshot below.
Do the same for the other button and select the other new function.
You can test the game and see if the buttons work. When that is done, you will notice that the buttons are able to work even with the start menu on the screen, so we need to disable the buttons while in the initialisation state and enable them in the playing state. To do this, first open the GameManager and add the following properties.
//the left button
@property({type:Node})
public oneStep:Node |null = null;
//the right button
@property({type:Node})
public twoStep:Node |null = null;
Save the GameManager and open the editor. Click on the Game Manager and see that the script will be expecting the button nodes in two sections. You will now have to drag the two buttons on these sections of the GameManager's inspector. See the screenshot below.
Now open the GameManager so that we disable the buttons when the init() function runs in the initialisation state. So go to the init() function and make it look as shown below.
init(){
if(this.startMenu){//the menu is true and not null
this.startMenu.active = true;//enables it
}
//disabling buttons
if(this.oneStep){
this.oneStep.active = false;
}
if(this.twoStep){
this.twoStep.active = false;
}
this.generateRoad();//generate road
if(this.playerCtrl){
//switch off mouse input
this.playerCtrl.setInputActive(false);
//reset player position to 0,0,0
this.playerCtrl.node.setPosition(Vec3.ZERO);
this.playerCtrl.reset();//moveIndex = 0
}
}
We can then enable the buttons when the game state is changed to the playing state. You can go to the setCurState() and add the code that enabled the buttons changing them to be active. See the code below.
set curState(value:GameState){
switch(value){
case GameState.GS_INIT:
this.init();//execute initialisation
break;
case GameState.GS_PLAYING:
if(this.startMenu){
this.startMenu.active = false;//disables the start menu
}
if(this.stepsLabel){
this.stepsLabel.string = '0';//reset to zero at the start of play
}
setTimeout(()=>{
if(this.playerCtrl){
this.playerCtrl.setInputActive(true);//enable the input
}
},0.1);//enable a small delay
//ENABLING THE BUTTONS
if(this.oneStep){
this.oneStep.active = true;
}
if(this.twoStep){
this.twoStep.active = true;
}
break;
case GameState.GS_END:
break;
}
}
The game should then be able to start without the left and right buttons and then when you click the play button, the buttons will then show up.This is what it will look like.
The complete code can be seen here.
import { _decorator, Component, instantiate,
Node, Prefab, Vec3, Label } from 'cc';
import { PlayerController } from './PlayerController';
const { ccclass, property } = _decorator;
enum BlockType{
BT_NONE,
BT_STONE,
};
enum GameState{
GS_INIT,
GS_PLAYING,
GS_END,
};
@ccclass('GameManager')
export class GameManager extends Component {
//cube prefab
@property({type:Prefab})
public cubePrfb: Prefab | null = null;
// reference to the PlayerController
@property({type:PlayerController})
public playerCtrl: PlayerController | null = null;
//reference to the start menu
@property({type:Node})
public startMenu: Node |null = null;
@property({type:Node})
public oneStep:Node |null = null;
@property({type:Node})
public twoStep:Node |null = null;
//total length of the road
public roadLength = 50;
private _road:BlockType[] = [];
//Label for the steps
@property({type:Label})
public stepsLabel:Label|null = null;
start() {
//current state is initiliazation
this.curState = GameState.GS_INIT;
this.playerCtrl?.node.on("JumpEnd",this.onPlayerJumpEnd,this);
}
onPlayerJumpEnd(moveIndex:number){
if(this.stepsLabel){
//casting to string from number, make sure we always score up to 50
this.stepsLabel.string = ''+(moveIndex >= this.roadLength?this.roadLength:moveIndex);
}
this.checkResult(moveIndex);
}
init(){
if(this.startMenu){//the menu is true and not null
this.startMenu.active = true;//enables it
}
//disabling buttons
if(this.oneStep){
this.oneStep.active = false;
}
if(this.twoStep){
this.twoStep.active = false;
}
this.generateRoad();//generate road
if(this.playerCtrl){
//switch off mouse input
this.playerCtrl.setInputActive(false);
//reset player position to 0,0,0
this.playerCtrl.node.setPosition(Vec3.ZERO);
this.playerCtrl.reset();//moveIndex = 0
}
}
set curState(value:GameState){
switch(value){
case GameState.GS_INIT:
this.init();//execute initialisation
break;
case GameState.GS_PLAYING:
if(this.startMenu){
//disables the start menu
this.startMenu.active = false;
}
if(this.stepsLabel){
//reset to zero at the start of play
this.stepsLabel.string = '0';
}
setTimeout(()=>{
if(this.playerCtrl){
//enable the input
this.playerCtrl.setInputActive(true);
}
},0.1);//enable a small delay
//ENABLING THE BUTTONS
if(this.oneStep){
this.oneStep.active = true;
}
if(this.twoStep){
this.twoStep.active = true;
}
break;
case GameState.GS_END:
break;
}
}
onStartButtonClicked(){
this.curState = GameState.GS_PLAYING;
}
checkResult(moveIndex:number){
//player position is not at beyond the end of the road
if(moveIndex < this.roadLength){
//check if the player is not on a road
if(this._road[moveIndex] == BlockType.BT_NONE){
this.curState = GameState.GS_INIT;
}
}else{//player is beyond the road
this.curState = GameState.GS_INIT;
}
}
generateRoad(){
//PRevent using old track
this.node.removeAllChildren();
this._road = [];
//First position has to be a stone
this._road.push(BlockType.BT_STONE);
for(let i = 1; i < this.roadLength;i++){
//if the previous position is an empty space
if(this._road[i-1]===BlockType.BT_NONE)
{
//then this position has to be a stone 1
this._road.push(BlockType.BT_STONE);
}else{
//randomize between 0 and 1
this._road.push(Math.floor(Math.random()*2));
}
}
//Generate tracks based on the runway
for(let j = 0; j < this._road.length;j++){
let block:Node = this.spawnBlockByType(this._road[j]);
if(block){
this.node.addChild(block);
block.setPosition(j,-1.5,0);
}
}
}
spawnBlockByType(type:BlockType){
if(!this.cubePrfb)
{
return null;
}
let block:Node |null = null;
//we can generate the road for real roads (1)
switch(type)
{
case BlockType.BT_STONE:
//instantiate block
block = instantiate(this.cubePrfb);
break;
}
return block;
}
update(deltaTime: number) {
}
}
import { _decorator, Component, EventMouse, input, Input, Node,
Vec3, Animation, SkeletalAnimation } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('PlayerController')
export class PlayerController extends Component {
//Animation Body
// @property({type:Animation})
// public BodyAnim: Animation | null = null;
//skeletal animations
@property({type:SkeletalAnimation})
public CocosAnim: SkeletalAnimation|null = null;
//check if player is jumping
private _startJump:boolean = false;
// The Jump Step (1 or 2 )
private _jumpStep:number = 0;
// The current position of the player
private _curPos:Vec3 = new Vec3();
// the target position of the player
private _targetPos:Vec3 = new Vec3();
//total jump time
private _jumpTime:number = 0.5;
//current jump speed
private _curJumpSpeed:number =0;
//current jump time
private _curJumpTime:number = 0;
//The difference of the current frame movement position during a jump
private _deltaPos:Vec3 = new Vec3(0,0,0);
//track the number of steps
private _curMoveIndex = 0;
start() {
//listen for click event
//input.on(Input.EventType.MOUSE_UP,this.onMouseUp,this);
}
setInputActive(active:boolean){//true or false
if(active){
//switch on mouse event listener
input.on(Input.EventType.MOUSE_UP,this.onMouseUp,this);
}else{
//switch off mouse event listener
input.off(Input.EventType.MOUSE_UP,this.onMouseUp,this);
}
}
leftButton(){
//left button clicked
this.jumpByStep(1);
}
rightButton(){
//right button clicked
this.jumpByStep(2);
}
onMouseUp(event:EventMouse)
{
//check if the user clicked the left (0) or the right (2) mouse button
if(event.getButton()===0)//checks for left
{
this.jumpByStep(1);
}
else if(event.getButton()===2)//checks for right
{
this.jumpByStep(2);
}
}
reset(){
this._curMoveIndex = 0;//when the game is over
}
jumpByStep(step:number)
{
if(this._startJump)//if the player is already jumping
{
// then we make sure the function does not make another jump
return;
}
//the following code only runs if the player is not jumping currently
//so that no other jump runs together
this._startJump = true;
// based on left or right click
this._jumpStep = step;
// Reset Jump time
this._curJumpTime = 0;
// current jump step
this._curJumpSpeed = this._jumpStep/this._jumpTime;
//get the position of the player
this.node.getPosition(this._curPos);
Vec3.add(this._targetPos,this._curPos, new Vec3(this._jumpStep,0,0));
//SKELETAL ANIMATION
if(this.CocosAnim){
this.CocosAnim.getState('cocos_anim_jump').speed = 3.5;
this.CocosAnim.play('cocos_anim_jump');
}
//Play Animation
/* if(this.BodyAnim){
if(step === 1){
this.BodyAnim.play("oneStep");
}else if(step === 2){
this.BodyAnim.play("twoStep");
}
}*/
//adds 1 or 2 to the current # of steps
this._curMoveIndex += step;
}
onOnceJumpEnd(){
if(this.CocosAnim){
this.CocosAnim.play('cocos_anim_idle');
}
//custom event
this.node.emit('JumpEnd',this._curMoveIndex);
}
update(deltaTime: number) {
if(this._startJump)// if _startJump is true
{
//Jump time = last jump time + frame interval
this._curJumpTime +=deltaTime;
if(this._curJumpTime>this._jumpTime)
{ //the jump has ended
//places the character where they should be at the end of a jump
this.node.setPosition(this._targetPos);
// make sure the jump state says we are not jumping
this._startJump = false;
this.onOnceJumpEnd();
}else{
//still jumping
//tweening = making smooth transitions between frames
//we have the player's position
this.node.getPosition(this._curPos);
// So that we know the length of the frame to displace
this._deltaPos.x = this._curJumpSpeed * deltaTime;
//setting the current position by adding the old current
//position plus the delta position (difference)
Vec3.add(this._curPos,this._curPos,this._deltaPos);
//set the node to a new position
this.node.setPosition(this._curPos);
}
}
}
}