游戏开发零散知识点和优化记录

简介

用来记录各类游戏开发中零散的知识点,和一些优化体验的方式等~

一、线性插值(Lerp)

之前看项目中有个动画用到线性插值的处理,好奇之下有什么作用效果,线性插值是什么意思,这里做学习记录。

是什么:

  • 简单了解来说,线性插值是知道两个值之间,按照比例计算中间值的方法。
  • 基本公式为: result = a + (b - a) * t,其中 a 为起始值, b 为终点值,而 t 是插值系数,通常为 0 - 1 之间。

怎么做: 那么我们来了解这里的使用案例来看看。这里我们做的一个功能是有一个锤子下落,并且会砸碎砖块,同时摄像机跟随锤子下落移动,那么我们每一次移动有一个下落距离,我们使用线性插值来调整动画时间,代码如下

ts 复制代码
    ....
    if (isNeedLerp) {
        time = this.lerp(0.3, 1, Math.abs(space) * 0.003);
    }
    ...

    /** 线性插值 */
    private lerp(a: number, b: number, space: number) {
        return a + (b - a) * this.clamp01(t)
    }
    /** 限制范围 0-1 之间 */
    private clamp01(value: number) {
        return this.clamp(value, 0, 1)
    }

    private clamp(num: number, min = 0, max = 1) {
        return Math.min(Math.max(num, min), max)
    }
    

这么做有什么效果如何呢~

1、 距离自适应:移动距离 space 越大,动画时间越长,但会在有一个我们的合理范围 [a-b] 之间

css 复制代码
-   小距离移动:接近最小时间(a秒),感觉更快捷
-   大距离移动:接近最大值(b秒),移动更平滑

2、 平滑感知 :根据Math.abs(space) * 0.003,将物理距离转换为合适的时间比例

diff 复制代码
-   确保短距离快速响应
-   长距离不会太快导致眩晕感

3、 提升用户体验

diff 复制代码
-   短距离迅速反馈,减少等待感
-   长距离平缓过渡,减少突兀感

为什么:

如果不使用线性插值处理这个动画时间呢?那么我们应该只有两种方式

1、移动时间是固定的

  • 短距离的小幅移动会显得缓慢拖沓
  • 长距离的大幅移动会显得突兀、眩晕

2、移动距离和时间呈线性关系

  • 短时间移动过快的闪烁感
  • 长距离缓慢拖沓

小结:

对于移动,人眼的感知是非线性的,使用线性插值,能从感知的视觉更自然的加速/减速的移动效果,快速的响应短距离,平稳的处理长距离,从玩家体验上带来更自然舒适的体验~上说的的是摄像机的跟随移动,另外其实很多地方我们也能用到,比如进度条的增长,或者一下平滑过动,控制速率的地方。可以说游戏的体验都是这些细节处理,带来的舒适体验!


二、关于按钮优化

怎么说呢~这个其实是个挺小挺简单的东西,但我感觉从用户体验上,能带来的收益很大,往往我们很多界面有关闭按钮,或者规则页按钮,这一类的按钮美术往往不会特别大,从美术风格出发,是为了美观协调。

但是那是美术的事情,我们程序开发的时候其实可以做成点击区域比美术按钮更大的,为什么呢!因为如果按钮点击区域过小,玩家不易点击,特别是钟爱小屏的玩家,或者平板用户,我们在观感上不用改变美术尺寸,只是把点击区域适当放大,就能很好的解决了。


三、做一个红点跳动效果

宝,很常见也很需要的一个效果了,实现一个红点弹跳的效果~其实也不难的东西,就是记录一下,方便后续要用到也不用再实现了。

js 复制代码
private _originalPosition = v3();
private _originalScale = v3();

onLoad() {
  this.record();
}

record() {
    // @ts-ignore
    this._originalPosition.set(this.node._lpos);
    // @ts-ignore
    this._originalScale.set(this.node._scale);
}

private playLittleAnimation() {
      tween(this.node)
          .to(0.2, { scale: v3(this._originalScale.x * 1.05, this._originalScale.y * 0.95, this._originalScale.z) })
          .parallel(
              tween(this.node)
                  .to(0.2, { position: v3(this._originalPosition.x, this._originalPosition.y + 15) }, { easing: "circOut" }),
              tween(this.node)
                  .to(0.12, { scale: v3(this._originalScale.x * 0.95, this._originalScale.y * 1.05, this._originalScale.z) })
          )
          .parallel(
              tween(this.node)
                  .to(0.15, { position: v3(this._originalPosition) }, { easing: "circIn" }),
              tween(this.node)
                  .delay(0.15)
                  .to(0.15, { scale: v3(this._originalScale.x * 1.05, this._originalScale.y * 0.95, this._originalScale.z) }, { easing: "backOut" })
          )
          .to(0.1, { scale: v3(this._originalScale) }, { easing: "backOut" })
          .delay(this.interval)
          .union()
          .repeatForever()
          .start();
  }

效果如图下:


四、cocos creator 如何做渐隐渐现

听起来好像这么简单的东西,这里我们来了解一下为啥子,要说这个 论坛中实现的方式有三种

可能我们一般上都是直接用到了 UIOption 组件来处理,但其实这不是最正确的方式,只能说是最合适的方式罢了,为什么呢,因为官方说了

那么这是为什么呢?

  • 方式1: 的问题在于我们的 color 暂时不支持 单独设置 alpha 值,所以暂时是没有效果的。

  • 方式2: 的问题是由于 tween 系统带来的问题,tween 系统会对值进行插值运算,但是和不凑巧的是,color 的各个颜色通道值只是我们为了方便理解的一个封装过的值,而实际存储 color 的值为其 _val,所以对于 tween 来说,他的 lerp 是针对 _val 值进行插值运算的,所以会出现颜色的五彩斑斓变化。

  • 方式3: 目前是可以使用的,但是为何我们不推荐使用呢,这是由于我们在系统设计中认为组件本身有 color 值,那么可以直接控制 color 来完成效果,而 UIOpacity 是毫无必要的,且会造成两个数据来源,所以我们不推荐使用,但由于之前两个方式存在问题,可以暂时使用这种方式。

目前正确的实现方式如下,但就没必要这么繁琐了~

js 复制代码
const originColor = this.sprite.color;
const targetAlpha = { value: 255 };
tween(targetAlpha)
    .to(
        1,
        { value: 0 },
        {
            onUpdate: (target, ratio) => {
                this.sprite.color = new Color(originColor.r, originColor.g, originColor.b, (ratio ?? 1) * 255);
            },
        }
    )
    .start();

五、如何做游戏表现快进

两种方式实现,首先说最简单粗暴的,修改导演的时间类

1、修改导演(director)的时间(tick)

这种方式最爽了,啥也不用改,简单,但是因为改的是总的速率,危险性比较高,避免离开加速后,离开或者出错后没有恢复原本的

js 复制代码
// 缓存 director 的 tick 方法
private oldTick = director.tick;

protected start(): void {
     // 重写 tick 方法
     director.tick = (dt: number) => {
         console.log("------------>tick", this.isSpeed);
         this.oldTick.call(director, dt * (this.isSpeed ? 1 : 5));
     }
}

// 结束恢复
protected onDestroy(): void {
 director.tick = this.oldTick;
}

2、过来就是对所有地方加速的操作了

一般加速都是动画的表现地方,比如使用了缓动 / spine / 音效,那么就需要对这些地方判断当前状态来加速了

js 复制代码
// spine 动画

let skeleton = this.anim.getComponent(sp.Skeleton);
skeleton.timeScale = this.isSpeed ? 2 : 1;

// 缓动的话就是直接执行的时间缩小了

tween(this.icon.node)
    .to(0.15 * this.speedValue, { scale: v3(1.15, 1.15, 1) })
    .to(0.15 * this.speedValue, { scale: v3(1, 1, 1) })
    .start();

六、如何做对称分布

有些时候我们需要对数组重新排序,使得这个排序后的物品能够对称分布,场景在一些物品摆放之类地方常见。那么如何实现这么一个算法处理呢

js 复制代码
/**对称排序宝石 */
private makeSymmetric(arr) {
    let counts = {};
    let singleArray = [];    // 单
    let doubleArray = [];    // 双
    let temp = {};

    // 把宝石分组
    for (let i = 0; i < arr.length; i++) {
        let id = arr[i].id;
        counts[id] = (counts[id] || 0) + 1;
        if (temp[id]) {
            temp[id].push(arr[i]);
        } else {
            temp[id] = [arr[i]];
        }
    }

    for (let id in counts) {
        while (counts[id] >= 2) {
            doubleArray.push(temp[id].shift(), temp[id].shift());
            counts[id] -= 2;
        }
    }

    for (let id in counts) {
        while (counts[id]-- > 0) {
            singleArray.push(temp[id].shift());
        }
    }

    // 实现对称摆放排列
    const resultArray = [];
    if (arr.length % 2 == 0) {
        // 特殊处理四个的时候不是两列对称
        if (doubleArray.length == 4) {
            resultArray.push(doubleArray[0], doubleArray[2], doubleArray[1], doubleArray[3]);
        } else {
            this.chunk(doubleArray, 2).forEach((item) => {
                resultArray.push(item[0]);
                resultArray.unshift(item[1]);
            })
            singleArray.forEach((item) => {
                resultArray.splice(resultArray.length / 2, 0, item)
            })
        }
    } else {
        this.chunk(doubleArray, 2).forEach((item) => {
            resultArray.push(item[0]);
            resultArray.unshift(item[1]);
        })
        let tempArr = singleArray.pop()
        singleArray.forEach((item) => {
            resultArray.splice(resultArray.length / 2, 0, item)
        })
        resultArray.unshift(tempArr);
    }

    // 小容错吧
    if (resultArray.length !== arr.length) {
        Logger.error("宝石排序出错", JSON.stringify(resultArray), JSON.stringify(arr));
        return arr;
    }
    return resultArray;
}

private chunk(arr, num) {
    let _arr = [];
    while (arr.length) {
        _arr.push(arr.splice(0, num))
    }
    return _arr
}

相关推荐
集成显卡1 小时前
PlayWright | 初识微软出品的 WEB 应用自动化测试框架
前端·chrome·测试工具·microsoft·自动化·edge浏览器
前端小趴菜052 小时前
React - 组件通信
前端·react.js·前端框架
Amy_cx2 小时前
在表单输入框按回车页面刷新的问题
前端·elementui
dancing9992 小时前
cocos3.X的oops框架oops-plugin-excel-to-json改进兼容多表单导出功能
前端·javascript·typescript·游戏程序
后海 0_o3 小时前
2025前端微服务 - 无界 的实战应用
前端·微服务·架构
Scabbards_3 小时前
CPT304-2425-S2-Software Engineering II
前端
小满zs3 小时前
Zustand 第二章(状态处理)
前端·react.js
程序猿小D3 小时前
第16节 Node.js 文件系统
linux·服务器·前端·node.js·编辑器·vim
萌萌哒草头将军3 小时前
🚀🚀🚀Prisma 发布无 Rust 引擎预览版,安装和使用更轻量;支持任何 ORM 连接引擎;支持自动备份...
前端·javascript·vue.js
狼性书生3 小时前
uniapp实现的简约美观的星级评分组件
前端·uni-app·vue·组件