Cocos Creator:添加光、影和骨架动画

Cocos Creator:添加光、影和骨架动画
推荐:将NSDT场景编辑器加入你的3D工具链
3D工具链:NSDT简石数字孪生

高级:添加光、影和骨架动画

在本节中,我们将向您展示如何使用动画等第三方资源来完成我们在快速入门:制作您的第一个 3D 游戏中制作的游戏原型

光照和阴影

光照和阴影是描述游戏的重要渲染功能。借助光源和阴影,我们可以模拟更逼真的游戏世界,提供更好的沉浸感和同理心。

接下来,我们为角色添加一个简单的阴影。

打开阴影

单击“层次结构”面板中的顶部节点,然后在“检查器”面板中选中“已启用”,并将“距离”和“法线”属性修改为:Sceneshadows

平面阴影

单击“播放器”节点下的“正文”节点,然后在组件中将“阴影投射模式”设置为“开”。cc.MeshRenderer

模型阴影

此时,您将在场景编辑器中看到一个阴影片。预览将显示阴影不可见,因为它直接位于模型后面,被胶囊覆盖。

玩家影子

调整照明

默认情况下,创建新场景时会添加带有组件的主光源节点。因此,为了使阴影出现在不同的位置,我们可以调整此光的方向。在“层次结构”面板中单击以选择“主光源”节点,并将属性调整为 (-10, 17, 0)。cc.DirectionalLightRotation

主灯

阴影效果可以通过单击预览来查看

播放器阴影预览

添加主角模型

导入模型资源

从原始源导入模型、材质、动画等资源不是本基础教程的重点,因此我们将直接使用已导入到项目中的资源。将项目(GitHub tutorial-mind-your-step-3d))放在 assets 目录中,并将 cocos 文件夹复制到你自己项目的 assets 目录中。

添加到场景

名为 Cocos 的预制件已包含在 Cocos 文件中。将其拖动到“层次结构”面板中“播放器”节点下的“正文”节点,作为“正文”节点的子节点。

添加可可斯预制件

此外,从“检查器”面板中删除原始胶囊模型:

取出胶囊

您可以在 Cocos 节点下添加一个聚光灯,以突出显示其闪亮的头部。

添加椰子灯

添加跳转动画

现在预告显示,主角最初会有一个待机动画,但是跳跃时用这个待机动画看起来会很不一致,所以我们可以在跳跃时改成跳跃动画。将变量添加到引用模型动画的类中:PlayerController.ts

@property({type: SkeletalAnimation})
public CocosAnim: SkeletalAnimation|null = null;

另外,由于我们已将主角从胶囊体更改为角色模型,因此我们可以丢弃之前为胶囊体创建的动画,并对相关代码进行注释,如下所示:

// @property({type: Animation})
// public BodyAnim: Animation|null = null;

jumpByStep(step: number) {
    // ...
    // if (this.BodyAnim) {
    //     if (step === 1) {
    //         this.BodyAnim.play('oneStep');
    //     } else if (step === 2) {
    //         this.BodyAnim.play('twoStep');
    //     }
    // }
}

然后在“层次结构”面板中将 Cocos 节点拖放到“播放器”节点的属性框中:CocosAnim

分配 Cocos 预制件

在脚本功能中播放跳转动画:jumpByStepPlayerController

jumpByStep(step: number) {
    if (this._startJump) {
        return;
    }
    this._startJump = true;
    this._jumpStep = step;
    this._curJumpTime = 0;
    this._curJumpSpeed = this._jumpStep / this._jumpTime;
    this.node.getPosition(this._curPos);
    Vec3.add(this._targetPos, this._curPos, new Vec3(this._jumpStep, 0, 0));

    if (this.CocosAnim) {
        this.CocosAnim.getState('cocos_anim_jump').speed = 3.5; // Jump animation time is relatively long, here to speed up the playback
        this.CocosAnim.play('cocos_anim_jump'); // Play jump animation
    }

    // if (this.BodyAnim) {
    //     if (step === 1) {
    //         this.BodyAnim.play('oneStep');
    //     } else if (step === 2) {
    //         this.BodyAnim.play('twoStep');
    //     }
    // }

    this._curMoveIndex += step;
}

这里的时间是0.3秒,如果动画长度和不匹配,可能会导致以下问题:_jumpStep_jumpStep

  • 动画尚未完成时,动画过渡不平滑
  • 或者动画完成但跳转时间尚未结束,导致滑点

处理此问题的一种方法是直接从动画剪辑的长度重新计算动画的速度,而不是使用常量:_jumpStep_jumpStep

var state = this.CocosAnim.getState('cocos_anim_jump');            
state.speed = state.duration/this._jumpTime;

开发人员可以自己尝试,也可以手动修改和正确的值来控制游戏的节奏。_jumpStepspeed

在脚本的功能中,使主角待机并播放待机动画。OnceJumpEndPlayerController

onOnceJumpEnd() {
    if (this.CocosAnim) {
        this.CocosAnim.play('cocos_anim_idle');
    }
    this.node.emit('JumpEnd', this._curMoveIndex);
}
注意:跳转完成后会触发,详见中的函数。onOnceJumpEndupdatePlayerController.ts

预览效果如下:
科科斯游戏

最终代码

PlayerController.ts

import { _decorator, Component, Vec3, input, Input, EventMouse, Animation, SkeletalAnimation } from 'cc';
const { ccclass, property } = _decorator;

@ccclass("PlayerController")
export class PlayerController extends Component {

    @property({type: Animation})
    public BodyAnim: Animation|null = null;
    @property({type: SkeletalAnimation})
    public CocosAnim: SkeletalAnimation|null = null;

    // for fake tween
    private _startJump: boolean = false;
    private _jumpStep: number = 0;
    private _curJumpTime: number = 0;
    private _jumpTime: number = 0.3;
    private _curJumpSpeed: number = 0;
    private _curPos: Vec3 = new Vec3();
    private _deltaPos: Vec3 = new Vec3(0, 0, 0);
    private _targetPos: Vec3 = new Vec3();
    private _curMoveIndex = 0;

    start () {
    }

    reset() {
        this._curMoveIndex = 0;
    }

    setInputActive(active: boolean) {
        if (active) {
            input.on(Input.EventType.MOUSE_UP, this.onMouseUp, this);
        } else {
            input.off(Input.EventType.MOUSE_UP, this.onMouseUp, this);
        }
    }

    onMouseUp(event: EventMouse) {
        if (event.getButton() === 0) {
            this.jumpByStep(1);
        } else if (event.getButton() === 2) {
            this.jumpByStep(2);
        }

    }

    jumpByStep(step: number) {
        if (this._startJump) {
            return;
        }
        this._startJump = true;
        this._jumpStep = step;
        this._curJumpTime = 0;
        this._curJumpSpeed = this._jumpStep / this._jumpTime;
        this.node.getPosition(this._curPos);
        Vec3.add(this._targetPos, this._curPos, new Vec3(this._jumpStep, 0, 0));

        if (this.CocosAnim) {
            this.CocosAnim.getState('cocos_anim_jump').speed = 3.5; // Jump animation time is relatively long, here to speed up the playback
            this.CocosAnim.play('cocos_anim_jump'); // Play jump animation
        }

        // if (this.BodyAnim) {
        //     if (step === 1) {
        //         this.BodyAnim.play('oneStep');
        //     } else if (step === 2) {
        //         this.BodyAnim.play('twoStep');
        //     }
        // }

        this._curMoveIndex += step;
    }

    onOnceJumpEnd() {
        if (this.CocosAnim) {
            this.CocosAnim.play('cocos_anim_idle');
        }

        this.node.emit('JumpEnd', this._curMoveIndex);
    }

    update (deltaTime: number) {
        if (this._startJump) {
            this._curJumpTime += deltaTime;
            if (this._curJumpTime > this._jumpTime) {
                // end
                this.node.setPosition(this._targetPos);
                this._startJump = false;
                this.onOnceJumpEnd();
            } else {
                // tween
                this.node.getPosition(this._curPos);
                this._deltaPos.x = this._curJumpSpeed * deltaTime;
                Vec3.add(this._curPos, this._curPos, this._deltaPos);
                this.node.setPosition(this._curPos);
            }
        }
    }
}

游戏管理器.ts

import { _decorator, Component, Prefab, instantiate, Node, Label, CCInteger, Vec3 } from 'cc';
import { PlayerController } from "./PlayerController";
const { ccclass, property } = _decorator;

// The runway type, pit (BT_NONE) or solid road (BT_STONE)
enum BlockType{
    BT_NONE,
    BT_STONE,
};

enum GameState{
    GS_INIT,
    GS_PLAYING,
    GS_END,
};

@ccclass("GameManager")
export class GameManager extends Component {

    // The runway prefab
    @property({type: Prefab})
    public cubePrfb: Prefab | null = null;
    // Length of the road
    @property({type: CCInteger})
    public roadLength: Number = 50;
    private _road: BlockType[] = [];
    // Node of the start menu
    @property({type: Node})
    public startMenu: Node | null = null;
    // The reference of the PlayerController instance on the Player node
    @property({type: PlayerController})
    public playerCtrl: PlayerController | null = null;
    // Label to display the step
    @property({type: Label})
    public stepsLabel: Label | null = null!;

    start () {
        this.curState = GameState.GS_INIT;
        this.playerCtrl?.node.on('JumpEnd', this.onPlayerJumpEnd, this);
    }

    init() {
        // Active the start menu
        if (this.startMenu) {
            this.startMenu.active = true;
        }
        // Generate the runway
        this.generateRoad();
        if(this.playerCtrl){            
            // Disable user input
            this.playerCtrl.setInputActive(false);
            // Reset the player's position
            this.playerCtrl.node.setPosition(Vec3.ZERO);
            // Reset the steps
            this.playerCtrl.reset();
        }
    }

    set curState (value: GameState) {
        switch(value) {
            case GameState.GS_INIT:
                this.init();
                break;
            case GameState.GS_PLAYING: 
                if (this.startMenu) {
                    this.startMenu.active = false;
                }

                if (this.stepsLabel) {
                    this.stepsLabel.string = '0';   // Reset the number of steps to 0
                }
                // What happens is that the character already starts moving at the moment the game starts
                // Therefore, a delay is needed here
                setTimeout(() => { 
                    if (this.playerCtrl) {
                        this.playerCtrl.setInputActive(true);
                    }
                }, 0.1);
                break;
            case GameState.GS_END:
                break;
        }
    }

    generateRoad() {        
        // Prevent the track from being the old track when the game is restarted
        // Therefore, the old track needs to be removed and the old track data cleared
        this.node.removeAllChildren();
        this._road = [];
        // Make sure that the character is standing on the real road when the game is running
        this._road.push(BlockType.BT_STONE);

        // Determine the type of track for each frame
        for (let i = 1; i < this.roadLength; i++) {
            // If the last track is a pit, then this frame must not be a pit
            if (this._road[i-1] === BlockType.BT_NONE) {
                this._road.push(BlockType.BT_STONE);
            } else {
                this._road.push(Math.floor(Math.random() * 2));
            }
        }

        // Generate tracks based on track type
        let linkedBlocks = 0;
        for (let j = 0; j < this._road.length; j++) {
            if(this._road[j]) {
                ++linkedBlocks;
            }
            if(this._road[j] == 0) {
                if(linkedBlocks > 0) {
                    this.spawnBlockByCount(j - 1, linkedBlocks);
                    linkedBlocks = 0;
                }
            }        
            if(this._road.length == j + 1) {
                if(linkedBlocks > 0) {
                    this.spawnBlockByCount(j, linkedBlocks);
                    linkedBlocks = 0;
                }
            }
        }
    }

    spawnBlockByCount(lastPos: number, count: number) {
        let block: Node|null = this.spawnBlockByType(BlockType.BT_STONE);
        if(block) {
            this.node.addChild(block);
            block?.setScale(count, 1, 1);
            block?.setPosition(lastPos - (count - 1) * 0.5, -1.5, 0);
        }
    }
    spawnBlockByType(type: BlockType) {
        if (!this.cubePrfb) {
            return null;
        }

        let block: Node|null = null;
        switch(type) {
            case BlockType.BT_STONE:
                block = instantiate(this.cubePrfb);
                break;
        }

        return block;
    }

    onStartButtonClicked() {
        // To start the game by clicking the Play button
        this.curState = GameState.GS_PLAYING;
    }

    checkResult(moveIndex: number) {
        if (moveIndex < this.roadLength) {
            // Jumped on the pit
            if (this._road[moveIndex] == BlockType.BT_NONE) {
                this.curState = GameState.GS_INIT;
            }
        } else {    // Jumped over the maximum length
            this.curState = GameState.GS_INIT;
        }
    }

    onPlayerJumpEnd(moveIndex: number) {
        if (this.stepsLabel) {
            // Because in the last step there may be a jump with a large pace, but at this time, whether the jump is a large pace or a small pace should not increase the score more
            this.stepsLabel.string = '' + (moveIndex >= this.roadLength ? this.roadLength : moveIndex);
        }
        // Check the type of the currently falling road and get the result
        this.checkResult(moveIndex);
    }

    // update (deltaTime: number) {
    //     // Your update function goes here.
    // }
}

3D建模学习工作室整理翻译,转载请标明出处!

上一篇:Cocos Creator:处理触摸事件 (mvrlink.com)

下一篇:Cocos Creator:场景资产 (mvrlink.com)

NSDT场景编辑器 | NSDT 数字孪生 | GLTF在线编辑器 | 3D模型在线转换 | UnrealSynth虚幻合成数据生成器 | 3D模型自动纹理化工具
2023 power by nsdt©鄂ICP备2023000829号