防抖机制的反思与改进:Cocos Creator 中方块跳跃误差
1. 问题描述
在游戏开发领域,玩家体验始终是核心关注点。为了提升游戏的流畅性和响应性,我在使用 Cocos Creator 开发时,引入了防抖机制来优化方块跳跃动画,相关细节可参考文章 《优化玩家体验:Cocos Creator 中的动画防抖策略》 。然而,深入开发和测试后发现,防抖机制虽减少了动画抖动,但导致新问题——方块跳跃距离不再严格保持设定的 40 个单位。为此,我决定调整策略,放弃防抖机制,转而采用非空判断。
经过调试和日志记录,我确认跳跃距离偏差并非由其他代码逻辑引起,而是防抖机制本身所致。
2. 原因分析
防抖机制通过延迟执行事件来防止多次触发,适用于按钮点击等场景。但在动画处理中,延迟执行会影响动画的起始时间,进而影响执行精度。
在方块跳跃动画中,期望每次跳跃精确移动 40 个单位。然而,防抖机制导致连续跳跃时部分动作被延迟,累积误差使跳跃距离不再精确。
3. 解决方案
为解决此问题,我取消防抖机制,采用非空判断确保动画准确执行。
3.1 非空判断逻辑实现
具体代码如下:
// 检查当前对象的 BodyAnim 属性是否存在
if (!this.BodyAnim) {
// 如果 BodyAnim 不存在,则直接返回,不执行后续代码
return;
}
此逻辑确保在执行跳跃动画前,BodyAnim 属性存在,避免因属性不存在导致的错误。同时,去除了防抖机制,保证跳跃动画及时执行,消除跳跃距离误差。
3.2 精准性与稳定性优化
引入非空判断后,方块跳跃的精准性和稳定性得到有效改善,每次跳跃更接近预期值,提升了玩家的操作体验。
3.3 完整代码示例
以下为包含非空判断逻辑的玩家控制器组件PlayerController.ts示例:
/**
* @author MYXH <1735350920@qq.com>
* @license GNU GPL v3
* @version 0.0.1
* @date 2024-12-30
* @description 玩家控制器
*/
import {
_decorator,
Component,
Vec3,
EventMouse,
input,
Input,
Animation,
} from "cc";
const { ccclass, property } = _decorator;
/**
* @description 添加一个放大比
*/
export const BLOCK_SIZE = 40;
@ccclass("PlayerController")
export class PlayerController extends Component {
/**
* @description 是否开始跳跃
*/
private _startJump: boolean = false;
/**
* @description 跳跃步数:一步或者两步
*/
private _jumpStep: number = 0;
/**
* @description 当前跳跃时间
*/
private _curJumpTime: number = 0;
/**
* @description 跳跃时间
*/
private _jumpTime: number = 0.1;
/**
* @description 移动速度
*/
private _curJumpSpeed: number = 0;
/**
* @description 当前的位置
*/
private _curPos: Vec3 = new Vec3();
/**
* @description 位移
*/
private _deltaPos: Vec3 = new Vec3(0, 0, 0);
/**
* @description 目标位置
*/
private _targetPos: Vec3 = new Vec3();
/**
* @description 身体动画
*/
@property(Animation)
BodyAnim: Animation = null;
/**
* @description 开始
* @returns void
*/
start() {
input.on(Input.EventType.MOUSE_UP, this.onMouseUp, this);
}
/**
* @description 重置
* @returns void
*/
reset() {}
/**
* @description 鼠标抬起事件
* @param event 鼠标事件
* @returns void
*/
onMouseUp(event: EventMouse) {
if (event.getButton() === 0) {
this.jumpByStep(1);
} else if (event.getButton() === 2) {
this.jumpByStep(2);
}
}
/**
* @description 跳跃
* @param step 跳跃的步数 1 或者 2
* @returns void
*/
jumpByStep(step: number) {
if (this._startJump) {
return;
}
this._startJump = true; // 标记开始跳跃
this._jumpStep = step; // 跳跃的步数 1 或者 2
this._curJumpTime = 0; // 重置开始跳跃的时间
const clipName = step == 1 ? "oneStep" : "twoStep"; // 根据步数选择动画
// 检查当前对象的 BodyAnim 属性是否存在
if (!this.BodyAnim) {
// 如果 BodyAnim 不存在,则直接返回,不执行后续代码
return;
}
const state = this.BodyAnim.getState(clipName); // 获取动画状态
this._jumpTime = state.duration; // 获取动画的时间
this._curJumpSpeed = (this._jumpStep * BLOCK_SIZE) / this._jumpTime; // 根据时间计算出速度
this.node.getPosition(this._curPos); // 获取角色当前的位置
Vec3.add(
this._targetPos,
this._curPos,
new Vec3(this._jumpStep * BLOCK_SIZE, 0, 0)
); // 计算出目标位置
// 播放动画
if (step === 1) {
// 调用 BodyAnim 的 play 方法,播放名为 "oneStep" 的动画
this.BodyAnim.play("oneStep");
} else if (step === 2) {
// 否则如果 step 等于 2
// 调用 BodyAnim 的 play 方法,播放名为 "twoStep" 的动画
this.BodyAnim.play("twoStep");
}
}
/**
* @description 更新
* @param deltaTime 时间间隔
* @returns void
*/
update(deltaTime: number) {
if (this._startJump) {
this._curJumpTime += deltaTime; // 累计总的跳跃时间
if (this._curJumpTime > this._jumpTime) {
// 当跳跃时间是否结束
// end
this.node.setPosition(this._targetPos); // 强制位置到终点
this._startJump = false; // 清理跳跃标记
} 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); // 将位移设置给角色
}
}
}
}
以下为游戏管理器组件GameManager.ts示例:
/**
* @author MYXH <1735350920@qq.com>
* @license GNU GPL v3
* @version 0.0.1
* @date 2025-01-17
* @description 游戏管理器
*/
import {
_decorator,
CCInteger,
Component,
Prefab,
Node,
instantiate,
} from "cc";
import { BLOCK_SIZE } from "./PlayerController";
const { ccclass, property } = _decorator;
/**
* @description 方块类型
*/
enum BlockType {
/**
* @description 无
*/
BT_NONE,
/**
* @description 石头
*/
BT_STONE,
}
@ccclass("GameManager")
export class GameManager extends Component {
/**
* @description 方块预制体
*/
@property({ type: Prefab })
public boxPrefab: Prefab | null = null;
/**
* @description 路径长度
*/
@property({ type: CCInteger })
public roadLength: number = 50;
/**
* @description 路径
*/
private _road: BlockType[] = [];
start() {
this.generateRoad();
}
/**
* @description 生成路径
* @returns void
*/
generateRoad() {
// 清除当前节点下的所有子节点
this.node.removeAllChildren();
// 初始化路径数组
this._road = [];
// startPos
this._road.push(BlockType.BT_STONE);
// 生成路径数组,根据前一个块类型决定当前块类型
for (let i = 1; i < this.roadLength; i++) {
if (this._road[i - 1] === BlockType.BT_NONE) {
// 如果前一个块是 BT_NONE,则当前块为 BT_STONE
this._road.push(BlockType.BT_STONE);
} else {
// 否则,随机生成 0 或 1
this._road.push(Math.floor(Math.random() * 2));
}
}
// 根据路径数组生成对应的块并添加到当前节点下
for (let j = 0; j < this._road.length; j++) {
let block: Node | null = this.spawnBlockByType(this._road[j]);
if (block) {
this.node.addChild(block);
// 设置块的位置,每个块之间的间隔为 BLOCK_SIZE
block.setPosition(j * BLOCK_SIZE, 0, 0);
}
}
}
/**
* @description 根据块类型生成块节点
* @param type 块类型
* @returns 块节点
*/
spawnBlockByType(type: BlockType) {
if (!this.boxPrefab) {
// 如果没有预制体,则返回 null
return null;
}
let block: Node | null = null;
// 根据块类型生成对应的块节点
switch (type) {
case BlockType.BT_STONE:
// 如果块类型是 BT_STONE,则生成 boxPrefab 的实例
block = instantiate(this.boxPrefab);
break;
}
return block;
}
update(deltaTime: number) {}
}
4. 结论
调整策略后,经多次测试确认,方块跳跃距离恢复至设定的 40 个单位,无累积误差。这一改变提升了游戏精度,改善了玩家操作体验。
此次调整使我深刻认识到,不同机制适用于不同场景。在游戏开发中,需根据实际情况灵活选择最合适策略,以确保玩家获得最佳体验。

