Cocos Creator 预制体变体(Prefab Variant)的替代方案

背景

在游戏开发中,我们经常需要基于一个基础预制体创建多个变体。比如:

  • 同一个敌人的不同等级版本

  • 同一个道具的不同属性版本

  • 同一个 UI 组件的不同样式版本

Unity 引擎提供了 Prefab Variant 功能来解决这个问题。但在 Cocos Creator 中目前还没有直接支持这个功能。今天和大家分享一个变通方案。

实现方案

基本思路

  1. 创建基础预制体(如 enemy.prefab)

  2. 为基础预制体添加 PrefabVariant 组件

  3. 创建变体预制体,将基础预制体拖入其中

  4. 在变体预制体中修改基础预制体的属性

  5. 使用时通过 PrefabVariant 组件获取实际节点

优势

  1. 基础预制体的修改会自动同步到所有变体中

  2. 在变体中的修改不会影响原始预制体

  3. 可以创建多个不同的变体

  4. 无需修改引擎,完全符合现有工作流

  5. 实现简单,代码量少

  6. 不会增加最终场景的节点层级深度

代码实现


// PrefabVariant.ts

import { Component, _decorator, Node } from "cc";

const { ccclass, property } = _decorator;

@ccclass('PrefabVariant')

export class PrefabVariant extends Component {

    // 直接返回当前节点,因为它就是我们要使用的预制体实例

    getNode(): Node {

        return this.node;

    }

}

使用方式

  1. 创建基础预制体,如 enemy.prefab

  2. 创建变体预制体,如 enemyFast.prefab

  3. enemy.prefab 拖入 enemyFast.prefab

  4. enemy.prefab 添加 PrefabVariant 组件

  5. 在变体预制体中直接修改 enemy 节点的属性

  6. 在代码中使用:


// 使用变体

let node = instantiate(enemyFastPrefab);

const variant = node.getComponentInChildren(PrefabVariant);

if (variant) {

    // 获取实际要使用的预制体节点

    node = variant.getNode();

}

// 现在可以直接使用 node

targetParent.addChild(node);

这种实现方式的优点:

  1. 代码更简洁,不需要额外的属性声明

  2. 不需要手动设置引用关系

  3. 直接使用节点的层级结构,更符合 Cocos Creator 的工作流

  4. 减少了出错的可能性

实际应用示例

假设我们要制作一个游戏,需要同一个敌人的多个变体:

  1. 基础敌人预制体 enemy.prefab:

@ccclass('Enemy')

export class Enemy extends Component {

    @property

    public moveSpeed = 100;

   

    @property

    public attackDamage = 10;

}

  1. 创建快速敌人变体:

    • 创建 enemyFast.prefab

    • enemy.prefab 拖入其中

    • 修改 moveSpeed 为 200

  2. 创建强力敌人变体:

    • 创建 enemyStrong.prefab

    • enemy.prefab 拖入其中

    • 修改 attackDamage 为 20

  3. 在游戏中使用:


// 根据难度生成不同的敌人

function spawnEnemy(difficulty: number) {

    let prefabAsset = normalEnemyPrefab;  // 默认普通敌人

    if (difficulty > 0.7) {

        prefabAsset = strongEnemyPrefab;  // 高难度出现强力敌人

    } else if (difficulty > 0.3) {

        prefabAsset = fastEnemyPrefab;    // 中等难度出现快速敌人

    }

   

    let node = instantiate(prefabAsset);

    const variant = node.getComponentInChildren(PrefabVariant);

    if (variant) {

        node = variant.getNode();

    }

    this.enemyLayer.addChild(node);

}

这种实现方式的优点:

  1. 代码更简洁,不需要额外的属性声明

  2. 不需要手动设置引用关系

  3. 直接使用节点的层级结构,更符合 Cocos Creator 的工作流

  4. 减少了出错的可能性

注意事项

  1. 合理控制预制体的嵌套层级,避免结构过于复杂

  2. 建议给变体预制体添加清晰的命名,如 EnemyVariant_Fast, EnemyVariant_Strong

  3. 可以通过组件继承来实现不同变体的特殊逻辑

  4. 记得及时清理不需要的变体预制体,避免项目结构混乱

  5. 建议将变体预制体按照功能分类存放,例如:

    
    assets/
    
    ├── prefabs/
    
    │   ├── enemy/
    
    │   │   ├── base/
    
    │   │   │   └── enemy.prefab
    
    │   │   └── variants/
    
    │   │       ├── enemyFast.prefab
    
    │   │       └── enemyStrong.prefab
    
    │   └── item/
    
    │       ├── base/
    
    │       │   └── item.prefab
    
    │       └── variants/
    
    │           ├── itemRare.prefab
    
    │           └── itemLegend.prefab
    
    

局限性

  1. 相比 Unity 的 Prefab Variant 功能,操作流程略显繁琐

  2. 需要额外的代码来管理变体逻辑

功能对比分析

与 Unity 的 Prefab Variant 相比,这套方案实现了大约 80% 的核心功能:

:white_check_mark: 已实现的功能:

  1. 基础预制体更新时变体自动同步

  2. 变体可以覆盖基础预制体的属性

  3. 变体的修改不会影响基础预制体

  4. 支持多级变体(变体嵌套)

  5. 运行时可以方便地访问和使用变体

:x: 未实现的功能:

  1. 没有专门的变体编辑界面

  2. 缺少可视化的属性覆盖提示

  3. 不支持在编辑器中直接将普通预制体转换为变体

  4. 没有变体属性还原功能(Unity中的Revert功能)

是否可以完全取代变体?

从功能实现的角度来看:

  • 对于中小型项目:完全可以取代,因为已经覆盖了主要的使用场景

  • 对于大型项目:基本可以取代,但在编辑器工作流方面会稍显不足

从开发效率的角度来看:

  • 优点:

    1. 实现简单,容易理解和维护

    2. 完全符合 Cocos Creator 的工作流

    3. 不需要修改引擎或使用复杂的插件

    4. 性能开销极小

  • 不足:

    1. 编辑器操作步骤相对繁琐

    2. 缺少可视化的变体管理工具

    3. 需要团队成员都理解并遵循这套规范

使用建议

  1. 对于新项目:

    • 可以直接采用这套方案

    • 建议在项目初期就建立好预制体和变体的管理规范

  2. 对于已有项目:

    • 如果项目中大量使用预制体变体,可以逐步迁移

    • 建议先在非关键功能中试用,验证可行性

  3. 最佳实践:

    • 制定清晰的变体命名和组织规范

    • 开发团队内部达成使用共识

    • 可以开发一些编辑器工具来辅助创建和管理变体

结语

这个方案虽然不如原生的预制体变体功能那么完善,但在当前版本的 Cocos Creator 中是一个实用的替代方案。希望这个方案能帮助到有类似需求的开发者。也期待 Cocos Creator 未来能够原生支持预制体变体功能。

欢迎大家在评论区分享你们的想法和建议!

1赞

这是预制体嵌套把

确实是嵌套. 主要是想在cocos中支持类似变体的功能. 只能通过嵌套去接近这个效果. 就是a修改后, 会同步到b. 这样结构可以放到a中, 参数可以放到b,c,d中.b,c,d继承a的结构, 并且重写a的属性.