JsonOb 响应式:数据劫持 + 变化监听完整技术细节

Screenshot_20260729_085722_tv_danmaku_bili_UnitedBizDetailsActivity.jpg

先纠正PPT笔误: Object.Defefirbairty  →  Object.defineProperty 
这套方案是Vue2 同源响应式方案,也是很多游戏引擎、小程序自研状态库 JsonOb 的底层实现。

一、整体流程对照PPT架构

plaintext

原始JS对象

Object.defineProperty 深度劫持(get/set)【数据拦截】

数据发生修改 → set触发 → 发送变更信号

信号广播系统(订阅发布模式)

通知所有依赖当前字段的UI组件,执行UI更新
 

二、核心1:Object.defineProperty 数据劫持原理

  1. API基础作用

 Object.defineProperty(obj, key, descriptor) 
对对象的指定属性重新定义,拦截 读取(get)、赋值(set)。

javascript

let rawData = { name:"test" };
// 劫持name属性
Object.defineProperty(rawData, 'name', {
get(){

// 读取时:收集当前正在执行的UI渲染函数(依赖收集)
track() 
return this._value;

},
set(newVal){

if(newVal === this._value) return; // 值无变化跳过
this._value = newVal;
// 修改时:触发信号派发,通知订阅者更新
trigger()

}
})
 

  1. 深度劫持(深层对象递归处理)

普通 defineProperty 只能劫持一层属性,嵌套对象  {a:{b:1}}  内层b不会监听。
实现方案:

1. 递归遍历对象所有属性;
2. 如果属性值依然是  Object/Array ,继续对它执行  defineProperty  包装;

这就是PPT写的【深度属性劫持】。

  1. 致命短板(defineProperty 天生缺陷)

1. 无法监听新增属性、删除属性
 obj.xxx = 1  新增字段不会触发set; delete obj.xxx  无钩子。
解决方案:框架提供  $set  /  $del  API。
2. 不能原生监听数组下标修改
 arr[2] = 999  不会触发set。

三、核心2:数组监听怎么实现(PPT第二条)

原生数组无法靠defineProperty监听下标变更,JsonOb采取 方法重写拦截:

1. 复制数组原型  Array.prototype ;
2. 重写会改变数组内容的变异方法: push pop shift unshift splice sort reverse ;
3. 当业务调用  arr.push()  时,先走重写后的包装函数:

  • 执行原生数组操作
  • 手动调用  trigger()  发送变更信号

javascript

// 伪代码思路
const rawPush = Array.prototype.push;
Array.prototype.push = function(...args){
const res = rawPush.apply(this, args);
trigger(this); // 广播数组变更
return res;
}
 

注意: arr[0] = xxx 、 arr.length = 10  依然无法被捕获,框架一般同样提供API规避。

四、核心3:依赖收集 & 信号广播(订阅发布)

劫持只是拿到“数据变化通知”,还需要知道哪些UI依赖这个字段。

  1. track() 依赖收集(get阶段)

1. UI组件执行渲染函数,读取响应式字段  data.name ;
2. 触发  get() ,执行 track ;
3. 把「当前渲染函数」存入当前属性的依赖列表。
数据结构简易模型:

plaintext

targetMap = {
对象: {

属性名: [组件更新回调1, 组件更新回调2]

}
}
 

  1. trigger() 信号广播(set阶段)

当 set 检测数值改变:

1. 获取该属性保存的所有依赖回调;
2. 批量执行回调 → UI组件执行刷新。
也就是PPT里的【信号广播】。

五、PPT重点特性:enableBatch 批量变更

场景:一帧内连续多次修改数据

plaintext

data.a = 1
data.b = 2
data.c = 3
 

不做批处理:每次赋值立刻触发UI刷新 → 3次渲染,性能浪费。
enableBatch实现逻辑:

1. 开启批量模式后, trigger 不会立刻执行更新;
2. 将所有变更任务存入队列;
3. 等到当前JS执行栈结束(下一帧/微任务时机);
4. 去重、合并所有变更,统一执行一次UI更新。

游戏引擎、前端渲染库高频修改场景极其重要,减少重绘次数。

六、内存管理(PPT最后一条)

这套响应式很容易产生内存泄漏:
对象销毁后,依赖列表依然保存UI组件引用,组件无法GC。
JsonOb处理方案:

1. 组件销毁时,主动清除对应属性的依赖回调;
2. 使用弱引用容器(WeakMap)保存targetMap;
3. 对外提供销毁API,解除对象劫持,清空订阅列表。

七、拓展对比:defineProperty vs Proxy

你这套JsonOb使用 Object.defineProperty(Vue2方案)
现代方案用  Proxy (Vue3):

1. Proxy原生支持监听新增属性、数组下标、删除;
2. 不需要递归深度劫持,可以惰性代理;
缺点:不支持低版本运行环境。
很多小游戏引擎、老框架继续沿用defineProperty实现的JsonOb。

八、完整数据流总结(对应PPT框图)

1. 开发者修改响应式数据;
2.  Object.defineProperty set  拦截赋值(数据拦截);
3. 判断值是否真正变化,避免无效通知;
4. 发送变更信号(批量模式下先入队列);
5. 信号广播遍历所有订阅该字段的UI组件;
6. 各个UI组件执行自身更新逻辑;
7. 组件卸载后自动清理依赖引用,防止内存泄露。

如果你需要,我可以写一份精简可运行的 JsonOb最小实现Demo(defineProperty版本),直观复现这套劫持+广播逻辑。

Cocos Creator 主动检测碰撞(3.8.8适用)

一、两种碰撞检测模式区分

1. 被动检测(默认):绑定碰撞组件+碰撞回调,物体接触自动触发onCollisionEnter
2. 主动检测(你要的):代码手动调用接口,任意时刻主动查询:有没有碰到、碰到哪些物体

二、2D主动碰撞检测(刚体/碰撞盒通用)

  1. 检测当前节点周围所有碰撞体(重叠查询)

typescript

import { _decorator, Component, Node, Collider2D, Physics2DManager, Rect } from 'cc';
const { ccclass, property } = _decorator;

@ccclass
export class Test extends Component {

start() {
    this.checkOverlap();
}

checkOverlap() {
    // 获取自身2D碰撞盒
    const collider = this.node.getComponent(Collider2D);
    if (!collider) return;

    // 主动查询所有和自己重叠的碰撞物体
    const resultArr = Physics2DManager.instance.queryColliders(collider.worldAABB);
    for (const coll of resultArr) {
        // 排除自己
        if (coll.node === this.node) continue;
        console.log("主动检测到碰撞目标:", coll.node.name);
    }
}

}
 

  1. 射线检测(主动发射射线探路)

适合:判断前方有没有障碍物、点击选中物体

typescript

// 从自身位置向右发射一条长射线
const origin = this.node.worldPosition;
const end = origin.add3f(200, 0, 0);
const hits = Physics2DManager.instance.raycast(origin, end);
if (hits.length > 0) {

console.log("射线碰到物体", hits[0].collider.node.name);

}
 

三、3D主动碰撞检测(软体、3D物体适用)

  1. 包围盒重叠检测

typescript

import { PhysicsSystem, Collider } from 'cc';

// 获取自身碰撞包围盒
const selfCollider = this.node.getComponent(Collider);
const box = selfCollider.worldBounds;

// 查询所有和盒子重叠的3D碰撞体
const hits = PhysicsSystem.instance.queryColliders(box);
for (const hit of hits) {

if (hit.node !== this.node) {
    console.log("主动检测碰撞3D物体:", hit.node.name);
}

}
 

  1. 3D射线检测

typescript

const start = this.node.worldPosition;
const dir = new Vec3(0, -1, 0); // 向下检测(落地判断)
const rayHit = PhysicsSystem.instance.raycastClosest(start, dir, 100);
if (rayHit) {

console.log("下方碰到地面/物体");

}
 

四、适用场景(对应你之前软体肉块需求)

1. 软体肉块掉落,主动向下射线检测:判断什么时候碰到桌子、凹凸物体
2. 肉块滑动时每帧主动查询周围碰撞体,贴合物体边缘
3. 不依赖自动碰撞回调,完全自己控制检测时机(每帧/间隔几秒检测一次)

五、关键注意点

1. 必须开启物理引擎:2D勾选 项目设置-物理2D;3D勾选物理系统
2. 所有需要被检测的物体,必须挂载对应 Collider2D / Collider 碰撞组件
3. 性能优化:

  • 不要每帧大规模查询全部碰撞体,能用射线就不用包围盒查询
  • 软体粒子多的话,隔2帧检测一次,降低消耗
    4. 区分:
  • queryColliders:检测重叠(两个物体贴在一起)
  • raycast:线段探测(预判前方物体)

六、简易落地检测示例(肉块掉桌面)

每帧向下射线,主动判断是否接触台面,接触后开启滑动软体动画:

typescript

update() {

const pos = this.node.worldPosition;
// 向下发射射线,长度50
const isHit = PhysicsSystem.instance.raycastClosest(pos, new Vec3(0,-1,0), 50);
if (isHit) {
    // 碰到物体,执行贴边滑动逻辑
}

}

Cocos Creator 3.8.8 自定义启动加载页(分3种方案,从简单替换LOGO到完整动画Loading场景)

先分清两个概念:

  1. 引擎闪屏Splash(构建后黑屏前那张Cocos图):Web/小程序/原生打包前的静态图片,项目设置一键替换;
  2. 游戏内Loading加载场景(资源加载进度条、动画、动态LOGO):引擎初始化完成后,加载贴图/预制体/关卡时的自定义页面,支持骨骼动画、液体/软体特效、渐变过渡(你做肉块液体项目优先用这个)

方案一:最简替换默认Splash闪屏(只换LOGO,无动画,5分钟搞定)

1. 编辑器内置配置(全平台通用:Web/微信小游戏/Android/iOS)

  1. 顶部菜单 项目 → 项目设置 → Splash Setting(插屏设置)
  1. 参数说明:
  2. 预览框点击加号,上传你的Logo图片(png/jpg,5M以内);
  3. TotalTime:最小显示毫秒,建议2000(至少显示2秒,不能太短);
  4. Image Size Multiplier:图片缩放比例,100%=原图,200%放大;
  5. 取消勾选可直接关闭引擎默认闪屏。
  6. 构建项目,打包后自动替换Cocos默认Logo。

2. Web端深度自定义(改背景、隐藏进度条、全屏图)

每次构建会覆盖build文件夹,必须用构建模板build-templates永久保存修改:

  1. 项目根目录新建文件夹:build-templates/web-mobile
  2. 先构建一次Web,复制build/web-mobile/index.htmlstyle-mobile.css到上面文件夹;
  3. 修改css,替换背景图、全屏、隐藏进度条:

    #splash {
     position: absolute;
     width: 100%;
     height: 100%;
     background: #000 url(./myLogo.png) no-repeat center;
     background-size: 80%; /* logo占屏幕80% */
    }
    /* 隐藏默认进度条 */
    .progress-bar {
     display: none !important;
    }
  4. 把你的logo图片放进build-templates/web-mobile/,下次构建自动生效。

方案二:完整游戏内Loading加载场景(推荐你的肉块液体项目,最强自定义)

优势

  • 支持骨骼动画、粒子液体、软体肉块、渐变淡入淡出;
  • 实时读取资源加载进度,自定义进度条、文字;
  • 不受引擎Splash限制,所有平台通用,微信小游戏/安卓/iOS都能用;
  • 可以复用前面写的全局数据单例控制加载参数。

完整实现步骤

步骤1:新建Loading场景

  1. 新建场景 Scene_Loading,设为启动场景(项目设置 → 项目数据 → 默认启动场景选Scene_Loading);
  2. 节点结构:

    Canvas
    ├─ Sprite_Bg 背景图
    ├─ Sprite_Logo 你的品牌logo(可加缓动缩放动画)
    ├─ ProgressBar 进度条组件(ProgressTimer)
    └─ Label_Progress 文字:0%

步骤2:加载脚本 LoadingManager.ts

核心逻辑:异步预加载所有游戏资源、实时更新进度、加载完成跳转主场景

import { _decorator, Component, ProgressTimer, Label, director, resources, Vec3, tween } from 'cc';
const { ccclass, property } = _decorator;

@ccclass('LoadingManager')
export class LoadingManager extends Component {
    @property(ProgressTimer) progressBar!: ProgressTimer;
    @property(Label) progressText!: Label;

    onLoad() {
        // Logo入场缩放动画
        const logo = this.node.getChildByName("Sprite_Logo");
        tween(logo)
            .set({ scale: new Vec3(0,0,0) })
            .to(0.6, { scale: new Vec3(1,1,1) })
            .start();

        // 开始预加载资源
        this.loadGameAssets();
    }

    // 批量预加载所有游戏资源(肉块预制体、液体贴图、场景)
    async loadGameAssets() {
        const bundleList = ["textures", "prefabs", "scenes"];
        let total = bundleList.length;
        let loaded = 0;

        for(const bundleName of bundleList) {
            await new Promise(res => {
                resources.loadBundle(bundleName, (finish, totalAll) => {
                    // 更新实时加载进度
                    const curPercent = (loaded + finish / totalAll) / total;
                    this.updateProgress(curPercent);
                }, ()=>{
                    loaded++;
                    res(null);
                })
            })
        }

        // 全部加载完成,跳转主游戏场景
        director.loadScene("Scene_Game");
    }

    // 更新进度条+文字
    updateProgress(percent: number) {
        this.progressBar.progress = percent;
        this.progressText.string = `${Math.floor(percent * 100)}%`;
    }
}

进阶扩展(适配你的软体/液体项目)

  1. 加载页放液体粒子、晃动肉块软体
    在Loading场景直接挂载之前写的液体发射器、弹簧质点肉块,加载资源时同步播放动态效果;
  2. 全局数据联动
    读取GlobalData里的液体粘度、肉块软度参数,在加载页预览默认效果;
  3. 过渡动画
    加载完成加淡出Tween,黑屏后再进入游戏场景,丝滑切换。

方案三:原生平台Android/iOS替换启动图(解决APP打开黑屏)

Android

  1. 构建安卓项目后打开proj.android/app/src/main/res/mipmap-xxx
  2. 替换所有ic_launcher、splash图片;
  3. 修改values/styles.xml设置全屏启动页背景色。

iOS

  1. Xcode打开工程,修改LaunchScreen.storyboard
  2. 替换启动图资源,移除Cocos默认占位图。

三种方案对比,按需选择

方案效果适用场景开发成本
方案一:替换引擎Splash静态图片,无动画,仅开机一瞬间只需要换个Logo,无动态效果需求极低
方案二:Loading加载场景骨骼动画、液体/软体、动态进度、转场动画你的肉块液体游戏、需要炫酷加载页中等(推荐)
方案三:原生启动图APP安装包冷启动黑屏阶段画面安卓iOS打包,解决黑屏

避坑关键点

  1. 不要混淆Splash和Loading场景
    引擎Splash是引擎初始化前,只能静态图;Loading场景是引擎跑起来之后,能做任意游戏动画;
  2. Web端直接修改build文件夹图片会失效
    必须用build-templates模板目录保存自定义html/css,否则每次构建覆盖;
  3. 加载场景资源尽量轻量化
    Loading场景只用1-2张小图,不要放大量贴图/物理预制体,避免加载页本身卡顿;
  4. 多场景数据共享
    Loading场景和游戏主场景共用之前写的GlobalData全局单例,加载进度、配置互通。

最简推荐流程(你的项目直接照做)

  1. 项目设置Splash替换自家Logo(解决开机Cocos标);
  2. 新建Scene_Loading作为启动场景,写上面LoadingManager脚本;
  3. 在Loading场景添加液体粒子、弹簧肉块软体做动态视觉;
  4. 预加载所有肉块、液体、关卡资源,进度条实时展示,加载完进入游戏。

Cocos Creator 3.8.8 多场景共享数据全套方案(分轻量/全局/持久化,适配你的软体、液体物理项目)

核心场景需求

  1. 切换场景(关卡A→关卡B)保留:液体粒子数量、肉块软体参数、玩家设置、物理配置、道具数据
  2. 场景并行叠加(additive叠加场景)互相读写数据
  3. 重启游戏数据不丢失(持久存储)
  4. 区分:临时运行时共享 / 本地永久保存

方案1:全局单例类(最推荐,运行时全局共享,所有场景通用)

原理

创建静态单例管理类,脚本挂载在「常驻根节点」(不会随场景销毁),所有场景直接导入调用,存液体、肉块、玩家、物理全局配置。

步骤1:创建全局数据管理器 GlobalData.ts

// assets/Script/GlobalData.ts
import { _decorator } from 'cc';
const { ccclass } = _decorator;

// 全局数据结构,统一管理所有跨场景数据
export type GameData = {
    // 你的软体肉块配置
    meatSoftConfig: {
        springFreq: number,
        damping: number
    },
    // 液体物理参数
    liquidConfig: {
        viscosity: number,
        dropCount: number
    },
    // 玩家通用数据
    playerScore: number,
    unlockLevel: number
}

@ccclass('GlobalData')
export class GlobalData {
    // 单例静态实例
    private static _instance: GlobalData;
    public static get instance() {
        if (!GlobalData._instance) {
            GlobalData._instance = new GlobalData();
        }
        return GlobalData._instance;
    }

    // 全局存储仓库
    public data: GameData = {
        meatSoftConfig: { springFreq: 3.5, damping: 0.5 },
        liquidConfig: { viscosity: 0.3, dropCount: 0 },
        playerScore: 0,
        unlockLevel: 1
    }

    // 快捷修改数据方法
    setMeatSoft(freq: number, damp: number) {
        this.data.meatSoftConfig.springFreq = freq;
        this.data.meatSoftConfig.damping = damp;
    }

    addLiquidDrop(num: number) {
        this.data.liquidConfig.dropCount += num;
    }

    // 清空临时运行数据(切换关卡重置用)
    clearTempData() {
        this.data.liquidConfig.dropCount = 0;
    }
}

步骤2:保证脚本常驻,任意场景读写

任意场景脚本读取/修改数据示例

import { GlobalData } from './GlobalData';

// 读取肉块软体参数
const softCfg = GlobalData.instance.data.meatSoftConfig;
console.log('当前软度', softCfg.springFreq);

// 修改液体粘度,切换下一个场景依然保留
GlobalData.instance.data.liquidConfig.viscosity = 0.8;

// 切换场景API(场景加载后数据不变)
director.loadScene("Level2");

关键:为什么不会被场景销毁?

这个类是纯TS逻辑类,不绑定任何节点,不依附场景,只要游戏进程不关闭,数据永久存在。
适合:关卡切换、多场景共用物理配置、软体/液体全局参数。

方案2:常驻节点(PersistRoot,带组件逻辑共享)

如果你不仅要存数据,还要全局持续运行脚本(全局液体发射器、全局物理检测、全局计时器),用常驻节点。

  1. 初始启动场景创建空节点 GameRoot
  2. 挂载代码:director.addPersistRootNode(this.node)

    import { _decorator, Component, director } from 'cc';
    const { ccclass } = _decorator;
    
    @ccclass('GameRoot')
    export class GameRoot extends Component {
     // 这里可以放全局数据 + 全局持续逻辑(液体全局生成、物理全局控制)
     public liquidTotal = 0;
    
     onLoad() {
         // 设置节点跨场景常驻,切换场景不会销毁
         director.addPersistRootNode(this.node);
     }
    }
  3. 任意场景获取常驻节点:

    const root = director.getScene().getChildByName("GameRoot")!.getComponent(GameRoot);
    root.liquidTotal += 10;

    适用场景:需要全局持续运行的物理、特效管理器。

方案3:附加式场景(Additive 叠加场景,互相直接访问节点数据)

如果你不是销毁切换场景,而是多层场景同时显示(UI场景 + 物理关卡场景并行):

// 加载叠加场景,两个场景同时存在,节点互通
director.loadScene("UIScene", { additive: true });

特点:

  1. 两个场景节点树共存,可以直接 find() 另一个场景的节点、组件读取数据;
  2. 缺点:场景多了层级混乱,不适合频繁切换关卡。

方案4:本地持久化存储(游戏关闭再打开依然保留数据)

上面两种只保内存,游戏关闭数据清空;需要永久存档用 sys.localStorage,搭配全局单例封装。

扩展全局单例,增加存档/读档

// 写入本地存档
saveToLocal() {
    const str = JSON.stringify(this.data);
    sys.localStorage.setItem("game_save", str);
}

// 启动读取存档
loadFromLocal() {
    const str = sys.localStorage.getItem("game_save");
    if(str) {
        this.data = JSON.parse(str);
    }
}

使用时机:关卡完成调用 saveToLocal();游戏启动第一帧调用 loadFromLocal()

四、四种方案对比,按需选择

方案优点缺点适用场景
纯TS单例GlobalData最轻量、无节点依赖、读写极快不能挂载持续运行的组件90%项目,跨场景共享配置、数值(肉块/液体参数首选)
常驻Persist节点可运行全局脚本、全局物理/特效管理器多常驻节点容易混乱需要全局持续逻辑(全局液体生成器、全局碰撞检测)
Additive叠加场景直接访问跨场景节点内存占用高,场景销毁麻烦UI与游戏场景分层、双场景同时显示
localStorage持久化永久保存,重启游戏不丢失只能存简单JSON,不能存对象/节点存档、玩家设置、解锁内容

五、实战避坑重点(踩坑总结)

  1. 不要把场景内节点、RigidBody、粒子放进全局数据
    切换场景后原场景节点会被销毁,保存引用会报错、内存泄漏;只存数值、配置参数,不存场景实体。
    正确:存液体粘度数字、弹簧刚度数值
    错误:存肉块质点Node、水滴预制体实例
  2. 多场景同时修改同一全局数据,数据冲突处理
    复杂项目加事件分发EventTarget,数据修改发送事件,所有场景监听自动刷新:

    import { EventTarget } from 'cc';
    export const gameEvent = new EventTarget();
    
    // 修改数据后派发事件
    GlobalData.instance.setMeatSoft(4,0.6);
    gameEvent.emit("meatConfigChange");
    
    // 其他场景监听数据更新
    gameEvent.on("meatConfigChange", ()=>{
     // 重新更新肉块软体弹簧参数
    })
  3. 切换场景清空临时数据
    每进新关卡执行 GlobalData.instance.clearTempData(),清除本关卡临时液体粒子计数,保留全局解锁、设置。
  4. 物理资源跨场景复用
    预制体(水滴、肉块)不要每个场景重复加载,在启动场景一次性加载存入全局单例,所有场景复用,减少卡顿。

六、你的软体/液体项目推荐组合(最优解)

  1. 基础:纯TS全局单例 GlobalData 存储所有软体、液体物理参数、玩家数据
  2. 全局特效:创建一个 Persist 常驻节点,挂载全局液体粒子管理器,全程生成水滴不中断
  3. 存档功能:单例封装 localStorage 实现配置永久保存
  4. 数据同步:搭配 EventTarget 事件,修改参数后自动刷新场景内所有肉块、液体物理效果

Cocos Creator 3.8.8 液体物理完整方案(适配你的肉块软体场景)

分三类方案,从低成本流体视觉 → 粒子流体 → 真实物理液体,按需选择,包含滴落、滩涂、沿斜坡流动、和肉块软体交互效果。
环境前提:3.8.8 内置 Ammo 3D物理 / Box2D 2D物理,无原生流体组件,全部靠扩展实现。

一、粒子模拟流体(90%小游戏首选,性能好、能沿斜面流动)

核心原理

大量带物理刚体的小球粒子模拟水滴/液体,重力下落、碰撞凹凸地面后受摩擦力沿坡面滑动、互相碰撞堆叠成水滩;可和之前弹簧质点肉块发生碰撞交互。

1、2D液体实现(Box2D)

  1. 粒子预制体

    • Node:水滴
    • RigidBody2D Dynamic,质量极小0.05
    • CircleCollider2D
    • 物理材质:摩擦力0.3、弹力0.05(低回弹,水不会乱弹)
  2. 发射器脚本:定时生成水滴粒子,设置初始速度
  3. 液体聚合效果

    • 粒子碰撞后叠加局部阻尼,落地后速度快速降低,堆积形成水洼
    • ChainCollider2D 凹凸斜坡自动引导粒子顺着边缘流动
  4. 销毁规则:停留地面超过5秒自动销毁,避免粒子爆炸卡顿

    2、3D液体实现(Ammo)

    水滴小球SphereCollider刚体,MeshCollider凹凸地形自动导流;
    增加粒子颜色叠加、透明度,大量粒子重叠模拟半透明水体。

    优缺点

    开发快、性能友好、支持和软体肉块碰撞、自动沿斜坡流动
    无流体压力、不会扩散摊开成薄水膜,仅点状水滴

简易生成水滴TS代码(3D)

import { _decorator, Component, Node, instantiate, Vec3 } from 'cc';
const { ccclass, property } = _decorator;

@ccclass('LiquidDropEmitter')
export class LiquidDropEmitter extends Component {
    @property(Node) dropPrefab!: Node;
    @property dropSpeed = 3;
    private timer = 0;

    update(dt: number) {
        this.timer += dt;
        if(this.timer > 0.1) {
            this.timer = 0;
            const drop = instantiate(this.dropPrefab);
            drop.setParent(this.node.parent);
            drop.setWorldPosition(this.node.worldPosition);
            // 随机横向偏移,模拟液体散开
            drop.getComponent(RigidBody)!.setLinearVelocity(new Vec3(
                (Math.random()-0.5)*this.dropSpeed,
                -this.dropSpeed,
                0
            ))
        }
    }
}

二、网格流体(2D 流体变形,滩涂、液体摊开,适合桌面积水)

实现方案: metaball 元球流体(视觉流体)

  1. 底层逻辑:多个水滴粒子作为元球,Shader叠加融合,视觉连成一整片液体;
  2. 物理层还是上面的刚体水滴,渲染层用后处理元球Shader融合,实现连续水面;
  3. 斜坡流动:刚体粒子沿凹凸地面滑动,元球跟随粒子位置实时更新,液体顺着台面边缘流淌;
  4. 和肉块交互:液体粒子碰到软体质点刚体产生碰撞,水被肉块挤开。

    局限

    仅视觉融合,无真实流体压强,无法模拟水压冲击、波浪。

三、SPH 光滑粒子流体动力学(真实物理液体,专业流体)

核心特性(真实水物理)

  • 具备密度、压力、粘度;液体挤压会向四周摊开,高处流向低处,斜坡自然流淌;
  • 撞击物体产生水花、飞溅;液体能包裹、推开软体肉块;
  • 粘度可调:低粘度清水、高粘度蜂蜜/粘稠酱汁(贴合你肉块软体需求)

    Cocos 3.8.8 两种接入方式

  • 开源JS SPH库 + Ammo物理联动(Web端)
    手写SPH计算循环,每一帧遍历所有液体粒子,计算压力、粘性力,更新粒子速度,再同步给Ammo刚体做碰撞;
  • 外部烘焙方案(性能优化)
    Blender烘焙流体动画,VAT顶点动画导入,仅播放固定水流动画,无法实时交互,不能和肉块动态碰撞

    缺点

    SPH每帧计算量大,粒子超过300移动端严重掉帧,仅适合少量液体特效。

四、Ammo 软体模拟粘稠液体(蜂蜜、酱料,软质流体)

承接你之前的肉块弹簧质点方案:
把大量微小质点用极软弹簧约束连接(Frequency=1.5,阻尼0.2),整体表现粘稠流体;

  • 低刚度弹簧:质点极易拉伸、摊开,像液体流动
  • 高阻尼:流动缓慢,粘稠不飞溅
  • 掉落在凹凸表面会顺着轮廓缓慢铺开,可和肉块软体互相挤压形变

五、关键细分需求调参指南

1. 清水(流动快、易散开)

粒子质量0.03,摩擦力0.2,弹力0.05,弹簧刚度极低(SPH粘度0.1)

2. 酱汁/粘稠液体(慢流、挂壁、贴合物体)

粒子质量0.08,摩擦力0.5,SPH粘度0.8;弹簧软体方案阻尼0.7

3. 沿凹凸桌面滑动核心设置

地面必须使用 ChainCollider2D(2D) / MeshCollider(3D)
禁用多个分离方块碰撞体,否则液体容易卡缝隙;刚体开启CCD连续碰撞防止穿模。

4. 液体与肉块软体交互

液体粒子刚体 和 肉块质点刚体 开启互相碰撞;
粘稠液体撞击肉块时,给肉块增加额外冲击力,实现肉块被水冲得晃动。

六、选型快速推荐

  1. 只需要滴水、水花、沿桌子流淌、和肉块简单碰撞 → 粒子刚体流体(方案一)(首选,低成本)
  2. 需要一整片积水、液体融合成水面视觉 → 粒子+元球Shader(方案二)
  3. 追求真实水压、波浪、粘稠酱汁摊开效果 → SPH流体(方案三,性能消耗高)
  4. 模拟果冻、粘液这类半固半液软物质 → 弹簧质点软体(承接你之前肉块方案)

七、性能优化

  1. 屏幕外液体粒子直接休眠刚体、停止计算;
  2. 单场景液体粒子上限控制在200以内;
  3. 移动端放弃SPH,只用简单粒子流体;
  4. 积水静态区域,超出5秒未动的粒子合并为静态面片,停止物理计算。