Most 3D modelling software such as blender allow the modeller to create skeletal animations. If 3D models imported into the project have skeletal animations, then we can use these animations in the game. I have linked all the assets of the project including the cocos 3D character that has skeletal animations. The cocos developers created this model with skeletal animations. Drag the folder named "Cocos" from the assets you downloaded into the assets section of your project on the bottom left. Open the folder that you have added to the project and look for a "Cocos" prefab. Click and drag this prefab ontop of the Body of our player in the node heirarchy as shown in the figure below.
You can then click on the Body and then go to the inspector, in the MeshRenderer click on the 3 dots and click remove so that the cube does not show on the screen. Check the figure below.
Switch to the 3D view so that we can add a spot light. Click on the Player and then right-click ->Create->Light->Spot Light.
Click on the spot light and change its rotation and position so that it points towards the player. You can manually move it or you can change the rotation and position numbers in the inspector. Because it is a child of the Player, it will follow the player movement.
We can now bring in skeletal animations. Open the PlayerController script. We need to make a reference to the skeletal animation component so add the following code.
//skeletal animations
@property({type:SkeletalAnimation})
public CocosAnim: SkeletalAnimation|null = null;
We will not be using the old animations anymore so we need to get rid of the reference. Comment it out by putting forward slashes at the front as shown below.
// @property({type: Animation})
// public BodyAnim: Animation|null = null;
You will also have to comment out sections of the jumpByStep function as shown below.
jumpByStep(step: number) {
// other code on top...
// if (this.BodyAnim) {
// if (step === 1) {
// this.BodyAnim.play('oneStep');
// } else if (step === 2) {
// this.BodyAnim.play('twoStep');
// }
// }
}
Save the file and then go to the editor. Click on the player node and go to the inspector. From there, you will be able to see that the PlayerController script will have a reference to the skeletal animation and so select the Cocos node from the Player and drag it to the reference as shown in the figure below.
Go to the PlayerController script and go to the jumpByStep() function. You will add the following code just before the code you commented out.
if(this.CocosAnim){
//animation time
this.CocosAnim.getState('cocos_anim_jump').speed = 3.5;
//the jump animation is named cocos_anim_jump
this.CocosAnim.play('cocos_anim_jump');
}
You can see that we first check if the CocosAnim is valid which makes sure the reference was added in the editor. We then set the speed for the animation to 3.5. We then play the animation.
The jumpByStep() function should look as shown below.
jumpByStep(step:number)
{
if(this._startJump)//if the player is already jumping
{
return;// then we make sure the function does not make another jump
}
//the following code only runs if the player is not jumping currently
this._startJump = true;//so that no other jump runs together
this._jumpStep = step;// based on left or right click
this._curJumpTime = 0;// Reset Jump time
this._curJumpSpeed = this._jumpStep/this._jumpTime;// current jump step
this.node.getPosition(this._curPos);//get the position of the player
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");
}
}*/
this._curMoveIndex += step;//adds 1 or 2 to the current # of steps
}
The final code for the PlayerController and the GameManager are as follows
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;
//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
}
//generate road
this.generateRoad();
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:
//execute initialisation
this.init();
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
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
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);
}
}
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
{
return;// then we make sure the function does not make another jump
}
//the following code only runs if the player is not jumping currently
this._startJump = true;//so that no other jump runs together
this._jumpStep = step;// based on left or right click
this._curJumpTime = 0;// Reset Jump time
// 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);
}
}
}
}