赞
踩
作者:hzb
开始时间:2023.12.12
最后一次更新时间:2023.12.12
MonoBehaviour
不好用MonoBehaviour
的强大之处MonoBehaviour
GameObject
上MonoBehaviour
的缺点Instantiating
物体时,会完全拷贝所有数据,消耗内存static
可以解决这个问题吗?在使用预制件时可能会不小心犯错误,例如意外地实例化(Instantiate)一个预制件。在Unity中,如果你不小心多次实例化了预制件,可能会导致场景中出现许多不必要的副本,这可能会导致性能问题或场景管理上的混乱。
预制件可能包含一些不必要的额外组件。例如,如果你只是想使用预制件中的一部分数据或资源,但预制件本身包含了许多其他组件(如碰撞体、渲染器等),这可能会使得使用变得复杂,并增加资源的负担。
预制件可能在概念上并不完全适合。预制件在Unity中通常用于创建和配置可以在场景中复用的游戏对象模板。但是,如果你的目的是仅仅存储和管理数据,而不是实例化游戏对象,那么预制件可能不是最佳选择,因为它们的设计初衷是为了实例化对象,而不仅仅是作为数据容器。
ScriptableObjects
是什么以及如何做到ScriptableObject
在Unity中作为数据管理和组织工具的优势ScriptableObject
拥有类似于 MonoBehaviour
的一些特性(比如可以序列化和在检视器中编辑),但它不是一个组件,这意味着它不能像 MonoBehaviour 那样附加到游戏对象上。
ScriptableObject
类非常简单,你只需要从 ScriptableObject
类派生,而不是从MonoBehaviour
类派生。与MonoBehaviour
不同,ScriptableObject
实例不能直接附加到游戏对象或预制件上。这使得它们更适用于作为数据的容器,而不是定义行为。
ScriptableObject
可以被序列化,这意味着它们的数据可以保存和加载。它们也可以在Unity检视器中编辑,就像 MonoBehaviour
一样,这为数据管理提供了便利。
ScriptableObject
的实例可以被保存为.asset
文件,这些文件可以在Unity项目中管理和分发,从而使数据重用和共享变得非常方便。
ScriptableObject
可以帮助解决一些涉及多态性的问题。例如,你可以创建一个 ScriptableObject
基类,并从它派生出具有不同数据和行为的子类。这样,你可以在运行时根据需要动态地引用不同的子类实例。
总体而言,使用ScriptableObject
作为一种在Unity中管理复杂数据和避免某些常见编程问题的方法。通过利用 ScriptableObject
,开发者可以更好地组织数据,减少依赖于场景的游戏对象,以及提高代码的灵活性和可维护性。
ScriptableObject
是如何解决这些问题的ScriptableObject
存储为项目资源,这意味着当退出播放模式(playmode)时,其存储的数据不会被重置,与在播放模式期间使用静态变量不同。ScriptableObject
可以被引用,而不是像在实例化(Instantiate)过程中那样被复制。这有助于避免不必要的数据副本,并且可以在运行时更高效地访问这些数据。ScriptableObject
可以在任何场景中被引用。这使得跨场景共享数据变得容易。ScriptableObject
的资源可以轻松地从一个项目传输到另一个项目。由于它们是独立的资源文件,这使得重用和共享设置或数据变得简单。ScriptableObject
是单独的文件,它们可以被版本控制系统(如Git)更有效地管理,而且每个文件的改变都能被精确地追踪。ScriptableObjects
ScriptableObject
using UnityEngine;
[CreateAssetMenu]
public class myScriptableObject : ScriptableObject {
public int someVariable;
}
MonoBehaviour
改为ScriptableObject
[CreateAssetMenu]
显示在资产的创建菜单中
ScriptableObject.CreateInstance<myScriptableObject>();
Callbacks
OnEnable
Reloading script
完成时OnDisable
Reloading script
时OnDestroy
ScriptableObject
的生命周期ScriptableObject
的生命周期与Unity中任何其他资源(如纹理、模型文件等)的生命周期相同。Resources.UnloadUnusedAssets
)来卸载。CreateInstance<>
创建,没有.asset文件):
HideFlags.HideAndDontSave
保持活跃状态。ScriptableObjects
的使用模式.asset
文件:
.asset
文件,让数据能够以资源的形式存储在Unity项目中。ScriptableObject
实例;另一种是一个ScriptableObject
实例包含了整个数据表。class EnemyInfo : ScriptableObject {
public int MaxHealth;
public int DamagePerMeleeHit;
}
class Enemy : MonoBehaviour {
public EnemyInfo info;
}
ScriptableObject
来创建可扩展枚举(Extendable Enums)ScriptableObject
实例主要用于等值比较。这类似于传统的枚举类型,但是以一种可以在编辑器中创建和管理的方式提供。ScriptableObject
实例可以轻松地扩展为包含更多信息的数据表。class AmmoType : ScriptableObject {}
if(inventory[Weapon.ammotype] == 0) {
PlayOutOfAmmoSount();
return;
}
inventory[weapon.ammoType] -= 1;
上面为Unity官方给出的简短例子,下面我将该例子补充完整。
创建一个空的ScriptableObject
绑定到.asset文件
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
[CreateAssetMenu]
public class AmmoType : ScriptableObject {}
定义Weapon
类
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Weapon : MonoBehaviour
{
public AmmoType ammoType;
public int damage;
}
实际使用时:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class TestEnum : MonoBehaviour { //记录当前武器的弹药类型还剩多少子弹 private Dictionary<AmmoType, int> ammoInventory; //当前武器 private Weapon weapon; //子弹类型 [SerializeField] private AmmoType[] ammoInventoryKeys; private void Awake() { //初始化字典 ammoInventory = new Dictionary<AmmoType, int>(); // 为每种弹药类型在字典中添加一个初始值 foreach (AmmoType ammoType in ammoInventoryKeys) { ammoInventory[ammoType] = 50; } } void Update() { if (ammoInventory[weapon.ammoType] == 0) { //播放没有子弹的音效 PlayOutOfAmmoSound(); return; } else { //播放射击音效 PlayShootSound(); //减少子弹数量 ammoInventory[weapon.ammoType]--; } } public void PlayOutOfAmmoSound() { //播放没有子弹的音效} } public void PlayShootSound() { //播放射击音效 } }
JsonUtility
类来序列化和反序列化 ScriptableObject
。这样做的好处是可以轻松地将 ScriptableObject
的数据转换为JSON格式,这种格式易于存储、传输和读取。ScriptableObject
通常保存为 .asset
文件作为项目资产。然而,在游戏运行时,你可能希望以JSON格式动态加载或保存这些数据。这可以使得在游戏运行时创建、编辑和保存用户生成的内容或游戏设置变得可能。.asset
文件保存,这样关卡就可以在Unity编辑器中编辑和打包。同时,游戏也可以允许玩家创建自己的关卡,并将这些关卡以JSON格式保存,便于共享或在不同的游戏会话间保持。[CreateAssetMenu]
class LevelData : ScriptableObject {
// 关卡数据的属性
}
LevelData LoadLevelFromFile(string path) {
// 从指定路径读取全部文本内容到一个字符串中
string json = File.ReadAllText(path);
// 创建一个LevelData的新实例
LevelData result = CreateInstance<LevelData>();
// 使用JsonUtility.FromJsonOverwrite将JSON字符串中的数据反序列化到result实例中
JsonUtility.FromJsonOverwrite(json, result);
// 返回填充了数据的LevelData实例
return result;
}
JsonUtility.FromJsonOverwrite
函数用于将JSON字符串中的数据覆盖到新创建的 LevelData
实例的相应字段中。这使得可以在运行时加载和修改游戏级别数据,而不仅限于在Unity编辑器中编辑 .asset
文件。ScriptableObject
创建一个全局访问点,并通过一个静态变量持有其实例。这种方法通常用于需要全局访问且不依赖场景中具体对象的情况。FindObjectOfType
方法来查找场景中的实例并重新赋值给静态变量。ScriptableObject
来创建代理对象(Delegate objects)ScriptableObject拥有方法:
ScriptableObject
被用来存储数据,但它们也可以包含方法,这些方法可以执行具体的逻辑。MonoBehaviour将自己传递给SO方法,SO完成工作:
MonoBehaviour
脚本可以调用 ScriptableObject
的方法,并将其自身作为参数传递。这样,ScriptableObject
可以根据传递进来的 MonoBehaviour
来执行特定的操作。允许可插拔和可配置的行为:
ScriptableObject
来在运行时更改这些行为示例:AI类型、能力增强/削弱:
ScriptableObject
。根据角色的当前状态或需要,你可以动态地切换这些AI对象。对于能力增强(buffs)或削弱(debuffs),你可以创建 ScriptableObject
来代表这些效果,并在运行时应用它们到角色上。官方示例:
定义一个抽象的 PowerupEffect:
PowerupEffect
的抽象类,它继承自 ScriptableObject
。这个类定义了一个抽象方法 ApplyTo
,该方法需要在子类中被重写,以便实现具体的能力增强效果。public abstract class PowerupEffect : ScriptableObject
{
public abstract void ApplyTo(GameObject go);
}
创建具体的 PowerupEffect 子类:
HealthBooster
类继承自 PowerupEffect
并重写了 ApplyTo
方法。这个方法增加了游戏对象的生命值public class HealthBooster : PowerupEffect
{
public int Amount;
public override void ApplyTo(GameObject go)
{
go.GetComponent<Health>().currentValue += Amount;
}
}
创建 MonoBehaviour 来使用 PowerupEffect:
Powerup
类是一个 MonoBehaviour
,它包含了一个 PowerupEffect
类型的字段。在触发器内发生碰撞时,它将调用 PowerupEffect
的 ApplyTo
方法来应用效果。public class Powerup : MonoBehaviour
{
public PowerupEffect effect;
public void OnTriggerEnter(Collider other)
{
effect.ApplyTo(other.gameObject);
}
}
补充完整上述案例,实现按E触发HealthBooster
并打印当前血量
定义一个抽象的 PowerupEffect:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
abstract public class PowerupEffect : ScriptableObject
{
abstract public void ApplyTo(GameObject p);
}
创建具体的 PowerupEffect 子类:
Create
菜单中的PowerEffects/HealthBooster
创建该脚本实例,默认名"NewHealthBooster"
using System.Collections; using System.Collections.Generic; using UnityEditor; using UnityEngine; [CreateAssetMenu(fileName = "NewHealthBooster", menuName = "PowerEffects/HealthBooster")] public class HealthBooster : PowerupEffect { //该buff的数值,可在实例中具体配置 public int amount; //buff具体效果,回复血量 public override void ApplyTo(GameObject p) { if (p.TryGetComponent<Health>(out Health health)) { health.CurrentValue += amount; } } }
创建血条组件:
currentValue
存放当前血量,同时设置共有字段CurrentValue
,并设置其set
的时候触发HealthChanged
事件OnHealthChanged
,并订阅事件HealthChanged
using System; using System.Collections; using System.Collections.Generic; using UnityEngine; public class Health : MonoBehaviour { //当血量改变时,触发的事件 public event EventHandler HealthChanged; private int currentValue = 100; public int CurrentValue { get { return currentValue; } set { currentValue = value; HealthChanged?.Invoke(this, EventArgs.Empty); } } //订阅事件 private void OnEnable() { HealthChanged += OnHealthChanged; } //取消订阅事件 private void OnDisable() { HealthChanged -= OnHealthChanged; } //OnHealthChanged 打印当前血量 public void OnHealthChanged(object sender, EventArgs e) { Debug.Log($"Current Health: {CurrentValue}"); } }
创建玩家类,并具体实现按E施加buff效果:
controller
PowerupEffect
实现多态OnInteracted
,其中调用buff的效果using System; using System.Collections; using System.Collections.Generic; using UnityEngine; public class Player : MonoBehaviour { public GameInput controller; public PowerupEffect powerupEffect; private void OnEnable() { controller.Interacted += OnInteracted; } private void OnDisable() { controller.Interacted -= OnInteracted; } private void OnInteracted(object sender, EventArgs e) { powerupEffect.ApplyTo(gameObject); } }
创建的NewHealthBooster
实例并配置数值
将baff配置挂载
运行,按E触发效果
ScriptableObject
的改进AudioEvent
public abstract class AudioEvent : ScriptableObject
{
public abstract void Play(AudioSource source);
}
using UnityEngine; using System.Collections; using Random = UnityEngine.Random; [CreateAssetMenu(menuName="Audio Events/Simple")] public class SimpleAudioEvent : AudioEvent { //存放具体的音频数组 public AudioClip[] clips; //随机获得的音量 public RangedFloat volume; //最大值和最小值 [MinMaxRange(0, 2)] public RangedFloat pitch; public override void Play(AudioSource source) { //如果没有音频 则返回 if (clips.Length == 0) return; //设置音频为其中的随机一个 source.clip = clips[Random.Range(0, clips.Length)]; //设置音量为上下限之间随机数 source.volume = Random.Range(volume.minValue, volume.maxValue); source.pitch = Random.Range(pitch.minValue, pitch.maxValue); //播放 source.Play(); } }
public abstract class DestructionSequence : ScriptableObject
{
//定义一个协程
public abstract IEnumerator SequenceCoroutine(MonoBehaviour runner);
}
[CreateAssetMenu(menuName="Destruction/Hide Behind Effect")] public class HideBehindEffect : DestructionSequence { // 粒子效果 public GameObject Effect; //具体Destroy时间 public float DestroyOriginalAfterTime = 1f; //具体协程实现 public override IEnumerator SequenceCoroutine(MonoBehaviour runner) { //生成粒子效果 Instantiate(Effect, runner.transform.position, runner.transform.rotation); //等待一段时间 yield return new WaitForSeconds(DestroyOriginalAfterTime); //Destroy建筑 Destroy(runner.gameObject); } }
public abstract class TankBrain : ScriptableObject
{
//虚函数可重写
public virtual void Initialize(TankThinker tank) { }
//抽象函数 必须重写
public abstract void Think(TankThinker tank);
}
具体写一个配置类
玩家控制的坦克
[CreateAssetMenu(menuName="Brains/Player Controlled")] public class PlayerControlledTank : TankBrain { //玩家编号 public int PlayerNumber; //该玩家的位移名称 private string m_MovementAxisName; //该玩家的选择名称 private string m_TurnAxisName; //该玩家的开火名称 private string m_FireButton; //初始化名称 public void OnEnable() { m_MovementAxisName = "Vertical" + PlayerNumber; m_TurnAxisName = "Horizontal" + PlayerNumber; m_FireButton = "Fire" + PlayerNumber; } // public override void Think(TankThinker tank) { //获取坦克的移动脚本组件 var movement = tank.GetComponent<TankMovement>(); //调用Steer函数 坦克行动 movement.Steer(Input.GetAxis(m_MovementAxisName), Input.GetAxis(m_TurnAxisName)); //获取坦克开火脚本组件 var shooting = tank.GetComponent<TankShooting>(); //开火 if (Input.GetButton(m_FireButton)) shooting.BeginChargingShot(); else shooting.FireChargedShot(); } }
简单狙击手AI
[CreateAssetMenu(menuName="Brains/Simple sniper")] public class SimpleSniper : TankBrain { // 瞄准角度阈值 public float aimAngleThreshold = 2f; // 每单位距离的充电时间 [MinMaxRange(0, 0.05f)] public RangedFloat chargeTimePerDistance; // 两次射击之间的时间 [MinMaxRange(0, 10)] public RangedFloat timeBetweenShots; // 重写TankBrain的Think方法 public override void Think(TankThinker tank) { // 获取坦克记忆中的目标 GameObject target = tank.Remember<GameObject>("target"); // 获取坦克的移动组件 var movement = tank.GetComponent<TankMovement>(); // 如果没有目标 if (!target) { // 寻找最近的非自身的坦克作为目标 target =GameObject .FindGameObjectsWithTag("Player") .OrderBy(go => Vector3.Distance(go.transform.position, tank.transform.position)) .FirstOrDefault(go => go != tank.gameObject); // 记住新的目标 tank.Remember<GameObject>("target"); } // 如果还是没有目标 if (!target) { // 没有目标,进行旋转 movement.Steer(0.5f, 1f); return; } // 瞄准目标 Vector3 desiredForward = (target.transform.position - tank.transform.position).normalized; // 如果目标角度大于阈值 if (Vector3.Angle(desiredForward, tank.transform.forward) > aimAngleThreshold) { // 判断旋转方向 bool clockwise = Vector3.Cross(desiredForward, tank.transform.forward).y > 0; // 旋转坦克 movement.Steer(0f, clockwise ? -1 : 1); } else { // 停止移动 movement.Steer(0f, 0f); } // 获取坦克的射击组件 var shooting = tank.GetComponent<TankShooting>(); // 如果不在充电 if (!shooting.IsCharging) { // 如果可以射击 if (Time.time > tank.Remember<float>("nextShotAllowedAfter")) { // 计算目标距离 float distanceToTarget = Vector3.Distance(target.transform.position, tank.transform.position); // 计算充电时间 float timeToCharge = distanceToTarget*Random.Range(chargeTimePerDistance.minValue, chargeTimePerDistance.maxValue); // 记住射击时间 tank.Remember("fireAt", Time.time + timeToCharge); // 开始充电 shooting.BeginChargingShot(); } } else { // 获取射击时间 float fireAt = tank.Remember<float>("fireAt"); // 如果到达射击时间 if (Time.time > fireAt) { // 射击 shooting.FireChargedShot(); // 记住下次可以射击的时间 tank.Remember("nextShotAllowedAfter", Time.time + Random.Range(timeBetweenShots.minValue, timeBetweenShots.maxValue)); } } } }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。