当前位置: 首页 > 工具软件 > Creature3D > 使用案例 >

3D沙盒游戏开发日志12——Prefab和GameComponent

微生恩
2023-12-01

日志

本篇将详细讲解第10篇日志中的Prefab与GameComponent部分(一定要先看第10篇)

如果说上一篇决策层的Brain和EventController是适用于其它生物和人物的话,那么本篇的Prefab和GameComponent就适用于整个游戏世界中所有物体(一颗树,一个斧头,一头狼,以及玩家角色)。这也是为什么角色和其它生物的决策层和动画层都要依靠GameComponent来实现功能,将整个游戏中的功能性代码与数据都集中在GameComponent中,模糊了生物与非生物的界限,也大大提高了代码的重用性以及debug方便性。至于GameComponent的特点以及意义在第10篇中已经说的很清楚了

GameComponent

public abstract class GameComponent : MonoBehaviour
{
    /*public bool Active
    {
        get => active;
    }
    private bool active;
    public void SetActive()
    {
        active = true;
    }
    public void SetInactive()
    {
        active = false;
    }*/
    public virtual void InitData(Dictionary<string, object> data)
    {

    }
    public virtual void SaveData(Dictionary<string, object> data)
    {

    }
}

可以看到我在GameComponent中几乎什么都没做!从之前提到的特征也能看出实际上用传统的Component(MonoBehaviour)也完全可以,但为什么要创建单独的GameComponent

  1. 为了未来的不时之需,如果未来我想要对GameComponent进行一些修改或者调试,因为GameComponent的数量太过庞大,一个一个改是不可能的,就可以利用基类以及我扩展的函数(我扩展了AddGameComponent等函数)来方便的修改
public static T AddGameComponent<T>(this GameObject gameObject) where T : GameComponent
{
    T temp = gameObject.AddComponent<T>();
    return temp;
}
  1. 为了区分,Health(GameComponent)和ActionController以及EventController显然不是一个类型也不是一个级别的组件,如果让他们都直接派生自MonoBehaviour成为兄弟显然是不合理的,所以用GameComponent可以区分这一类Component。
  2. 为了数据序列化(与Prefab配合进行),虽然可以创建一个接口来让需要持久化数据的Component实现接口,但我不是很喜欢使用接口,接口在unity中的支持很差(比如findobjectoftype就不能查找接口对象),当然使用基类也有弊端就是有些不需要序列化的组件也会被调用空函数(太多的话可能造成性能压力)

PrefabComponent

每个游戏物体的包含一个PrefabComponent,它就像身份证是唯一能证明斧子是斧子的组件,它非常重要,主要负责以下事情

  1. 初始化所有组件,包括像ActionController这样的系统组件和GameComponent,如果是生物的话还有Brain。负责配置组件的参数,包括一个默认配置(游戏刚开始)和读取存档数据的初始化(调用GameComponent的InitData)
  2. 注册简单事件处理(具体见于第10篇EventHandler部分)
  3. 维护prefab的数据,包括所有GameComponent的数据和Prefab本身的数据(transform,loadPath等)
  4. 包含一组标签,之前说过GameComponent的作用之一就是标签,但GameComponent代表的主要是功能性标签(即能做什么),而Prefab包含一组性质标签(即指示该物体是生物,是邪恶生物是飞行生物等),并且该标签也是可变的,即人物可能临时被添加一个飞行标签。
  5. 包含一个加载路径,因为本项目使用的是AssetBundle资源管理,而我的打包策略是每个Prefab所带有的所有资源一个包,所以PrefabComponent内包含一个AB包名(一般就是Prefab名),同时也会用该路径加载初始化所需的资源

不难看出,PrefabComponent最主要的任务集中在游戏开始时和结束时,但更重要的是,因为一定所有游戏物体都带有该组件,我可以将标签(GameTag)等这样的全局特性应用到PrefabComponent上(未来也许会有更多的属性)

[FlagsAttribute]
public enum GameTag
{
    creature = 0x0001,
    player = 0x0002,
    neutral = 0x0004,
    evil = 0x0008
}
public abstract class PrefabComponent : MonoBehaviour
{
    public class PersistentPrefabData
    {
        //pos
        public float x;
        public float y;
        public float z;
        //rot
        public float pitch;
        public float yaw;
        public float roll;
        //AssetName(AssetBundle)
        public string loadPath;
        //all data of gamecomponents it have
        public Dictionary<string, object> componentData;
        void Generate()
        {
            GameObject target = ABManager.Instance.LoadAsset<GameObject>("Prefab", loadPath);
            GameObject go = GameObject.Instantiate(target, new Vector3(x, y, z), Quaternion.Euler(pitch, yaw, roll));
            PrefabComponent pc = go.GetComponent<PrefabComponent>();
            pc.Init(componentData);
        }
    }
    public abstract string bundleName {get;}
    private GameTag gameTag;
    public void Init(Dictionary<string, object> componentData)
    {
        DefaultInit();
        if(componentData != null) InitComponentsData(componentData);
    }

    void Awake()
    {
        Init(null);
    }
    public void AddTag(GameTag tag)
    {
        gameTag |= tag;
    }
    public bool HasTag(GameTag tag)
    {
        return (gameTag & tag) != 0;
    }

    public string GetPersistentData()
    {
        PersistentPrefabData data = new PersistentPrefabData();
        data.x = transform.position.x;
        data.y = transform.position.y;
        data.z = transform.position.z;
        data.pitch = transform.rotation.eulerAngles.x;
        data.yaw = transform.rotation.eulerAngles.y;
        data.roll = transform.rotation.eulerAngles.z;
        data.loadPath = bundleName;
        foreach (var item in GetComponents<GameComponent>())
        {
            item.SaveData(data.componentData);
        }
        return JsonConvert.SerializeObject(data);
    }
    public abstract void DefaultInit();
    public void InitComponentsData(Dictionary<string, object> componentData)
    {
        foreach (var item in GetComponents<GameComponent>())
        {
            item.InitData(componentData);
        }
    }
}

举两个例子看一下PrefabComponent具体是如何工作的
石头人

public class GolemEarth : PrefabComponent
{
    private GolemEarthBrain brain;
    public override string bundleName
    {
        get => "GolemEarth";
    }

    public override void DefaultInit()
    {
        EventHandler eventHandler = gameObject.AddComponent<EventHandler>();
        eventHandler.ListenEvent("hasmineraround", OnHasMinerAround);
        ActionController actionController = gameObject.AddComponent<ActionController>();
        //actionController.debug = true;

        Locomotor locomotor = gameObject.AddGameComponent<Locomotor>();
        locomotor.defaultMoveSpeed = Constants.golem_move_speed;
        locomotor.debug = true;
        RememberLocation rememberLocation = gameObject.AddGameComponent<RememberLocation>();
        rememberLocation.Remember("spawnpoint", transform.position);
        Combat combat = gameObject.AddGameComponent<Combat>();
        InitCombat(combat);
        Health health = gameObject.AddGameComponent<Health>();
        health.maxHealth = Constants.golem_health;
        health.health = health.maxHealth;

        brain = gameObject.AddComponent<GolemEarthBrain>();
        //brain.debug = true;
        brain.Begin();
    }
    private void InitCombat(Combat combat)
    {
        Combat.Config defau = new Combat.Config();
        defau.attackCD = Constants.golem_attack_cd;
        defau.attackDistance = Constants.golem_attack_distance;
        defau.hitDistance = Constants.golem_hit_distance;
        defau.attackDuration = Constants.golem_attack_duration;
        defau.damage = Constants.golem_attack_damage;
        Combat.Config spin = new Combat.Config();
        spin.attackCD = Constants.golem_spin_cd;
        spin.attackDistance = Constants.golem_spin_attack_distance;
        spin.hitDistance = Constants.golem_spin_hit_distance;
        spin.attackDuration = Constants.golem_spin_attack_duration;
        spin.damage = Constants.golem_spin_attack_damage;
        combat.CreateConfig("default", defau);
        combat.CreateConfig("spinattack", spin);
        combat.minAttackGap = Constants.normal_min_attack_gap;
    }
    private void OnHasMinerAround(params object[] args)
    {
        if(GetComponent<Combat>().IsValidTarget()) return;
        GameObject target = args[0] as GameObject;
        gameObject.GetComponent<Combat>().target = target;
    }
}

石头人是非常简单的生物,像Combat这样的组件基本生物都有而且初始化项很多,也有些组件不需要初始化,除此外还注册了聆听周围采矿事件的处理
镐子

public class Pickaxe : PrefabComponent
{
    public override string bundleName
    {
        get => "Pickaxe";
    }

    public override void DefaultInit()
    {
        AnimatorOverrideController pickaxeAnimator = ABManager.Instance.LoadAsset<AnimatorOverrideController>(bundleName, "OverrideAnim");
        Sprite inventoryIcon = ABManager.Instance.LoadAsset<Sprite>(bundleName, "InventoryIcon");

        Pickable pickable = gameObject.AddGameComponent<Pickable>();
        pickable.type = PickType.Pickup;

        InventoryItem inventoryItem = gameObject.AddGameComponent<InventoryItem>();
        inventoryItem.icon = inventoryIcon;

        Weapon weapon = gameObject.AddGameComponent<Weapon>();
        Combat.Config conf = new Combat.Config();
        conf.damage = Constants.pickaxe_damage;
        conf.attackDuration = Constants.tool_attack_duration;
        conf.attackDistance = Constants.tool_attack_distance;
        conf.hitDistance = Constants.tool_hit_distance;
        weapon.combatConf = conf;

        Tool tool = gameObject.AddGameComponent<Tool>();
        tool.toolTypes = new List<WorkToolType>{ WorkToolType.Pickaxe };

        FiniteUse finiteUse = gameObject.AddGameComponent<FiniteUse>();
        finiteUse.maxUse = 50;
        finiteUse.currUse = 50;

        Equipable equipable = gameObject.AddGameComponent<Equipable>();
        equipable.equipSlotType = EquipSlotType.Hand;
        equipable.overrideAnimator = pickaxeAnimator;
        equipable.pos = new Vector3(0.005f, -0.019f, 0.38f);
        equipable.rot = new Quaternion(0.5f, -0.5f, -0.5f, 0.5f);
        
        Inspectable inspectable = gameObject.AddGameComponent<Inspectable>();
        inspectable.inspectStr = "nxsnb,nxsnb  nxsnxs";
        inspectable.rimLightMat = ABManager.Instance.LoadAsset<Material>("Material", Constants.default_rimLightMat_path);
    }
}

对于非生物而言,没有EventController和ActionController,也没有brain,其它没有区别。PrefabComponent内一般不包含需要持久化的字段。

 类似资料: