Unity中动态修改游戏中任意参数的框架

本文介绍了一种在游戏运行过程中实时修改游戏参数的方法。通过控制台输入指令即可修改指定类中的变量,支持整型、浮点型、字符串和布尔型。该框架采用消息机制解耦对象,提供了一键生成代码的功能。

本文章由cartzhang编写,转载请注明出处。 所有权利保留。
文章链接:http://blog.csdn.net/cartzhang/article/details/56292977
作者:cartzhang

一、 引言


心血来潮,根据之前做的一个控制台的输入,就想根据控制台的输入,来控制和修改游戏中的某些参数。

目前大部分简单游戏修改参数有数据库,XML,json等各式各样,现在做的是一个可以在游戏过程中,实时修改任意变量的一个东西。

用法很简单,tab按键来打开控制台,输入需要修改的参数就可以,看到参数实时被修改。
也可以把它理解成简单的修改器,用于调节参数和可能的命令修改游戏中的参数。

当然,也需要稍微修改一个项目中原有的代码。这个功能已经使用代码一键实现了,并且使用unity中不加入编译的方式,在代码文件名字前加“.”的方式保留了原来的代码。

先看下整体的思维导图

这里写图片描述
思维导图

二、输入部分


输入部分有控制台来实现输入。

这里写图片描述

图0

在ConsoleInput类中添加了OnInputText时间,这样就可以根据输入不同进行处理了。

关于具体控制台程序的一些用法和使用事项,请参考后面给出的github和博客地址,里面进行了说明和更新,也非常期待感兴趣的可以自己提交更改。

    void OnInputText( string obj )
    {
        this.ConsolePrint(obj);        
        if (!NotifyToChangeVarialbe(obj))
        {
            this.ConsolePrint("not correct input to change variable");
        }
    }


顺便说下,this.ConsolePrint是对MonoBehaviour类进行了扩展。
具体代码:


public static class ExtendDebugClass
{
    public static void ConsolePrint(this MonoBehaviour mono, string message)
    {
        if (message.Length < 0) return;
        System.Console.WriteLine(message);
    }
}

三、输入转换


输入的字符串作为参数,需要不同进行处理。
输入的参数有三部分组成,类名称,变量名,变量新值。

目前只可以处理整形,浮点型,字符串和布尔型四种常用类型。

 public static bool IsVarialbeInList(string classname,string classname_variableName, out string valuetype)
    {
        bool bresult = false;
        string tmpclassName = classname;
        Debug.Assert(!string.IsNullOrEmpty(tmpclassName));
        Debug.Assert(!string.IsNullOrEmpty(classname_variableName));
        valuetype = null;
        if (CollectAttributeUtil.myFuctionList.ContainsKey(tmpclassName))
        {
            List<AttributeForClass> mlist = (List<AttributeForClass>)myFuctionList[tmpclassName];
            for (int i = 0; i < mlist.Count; i++)
            {
                if (mlist[i] != null &&
                    string.Compare( mlist[i].class_variable ,classname_variableName,true) == 0)
                {   
                    valuetype = CheckVariableType(mlist[i].variable_type);
                    bresult = true;
                    break;
                }
            }
        }
        return bresult;
    }


然后进行消息触发,就搞定了修改参数,并且可以实时修改。

四、收集可修改变量


这里自定义类一个字段属性ModifyAttribute
ModifyAttribute.cs

using System;

[System.AttributeUsage(AttributeTargets.All)]
public class ModifyAttribute : Attribute
{
    //public ModifyAttribute()
    //{

    //}

}


若使用这个框架,只需在需要改变的变量代码前添加这个属性描述。
例如:

[ModifyAttribute]
    public int clolor;

    [ModifyAttribute]
    public string attack;


下面这个函数负责收集所有的可修改变量,在ServerConsole.cs中的Awake中被调用。

CollectAttributeUtil.InitialCollectModifyAttribute();


收集变量的主要代码在CollectAttributeUtil.cs中实现。

代码不是很多,但是很重要。
主要的是这个

/// <summary>
    /// get attribute type variable in each monobehaviour class.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="p"></param>
    private static void CollectAttributeProperty<T>(MonoBehaviour p) where T : ModifyAttribute
    {
        var flags = BindingFlags.Instance  | BindingFlags.Public | BindingFlags.Static;
        var type = p.GetType();
        FieldInfo[] fieldInfor = type.GetFields(flags);
        foreach (var field in fieldInfor)
        {
            var objs = field.GetCustomAttributes(typeof(T), false);
            if (objs.Length > 0)
            {
                string keyTmp = type.FullName;
                AttributeForClass attri4Class = new AttributeForClass();
                attri4Class.class_variable = (type.FullName + "#" + field.Name);
                attri4Class.variable_type = field.FieldType;
                if (!myFuctionList.Contains(keyTmp))
                {
                    List<AttributeForClass> AttriClassList = new List<AttributeForClass>();
                    AttriClassList.Add(attri4Class);
                    myFuctionList.Add(keyTmp, AttriClassList);
                }
                else
                {
                    List<AttributeForClass> mlist = (List<AttributeForClass>)myFuctionList[keyTmp];
                    mlist.Add(attri4Class);
                }
                //Debug.Log(type.ToString() + " current varible is  " + field.Name + " type is " + field.FieldType);
            }
        }
    }


这个函数把所有收集对象按类的名字为基础,然后形成列表,存放到myFuctionList中,等待被使用。

五、编辑器模式下的自动生成代码


由于使用了消息机制,需要自动来把需要修改的变量,写为函数,然后在注册为消息。
不用担心,这些工作已经做为了。只需要按下一个按键,一键搞定。

在Editor文件夹下,有个CreateCode.cs文件,且只可以在编辑器模式下运行。

这里写图片描述

图3

这里的代码还是蛮多,说下主要的,

第一,得到所有需要注册的类名和变量名,这个工作其实代码已经写过了就是前面的收集工作。这里只需要调用一下就可以了。

第二,创建partial文件夹,根据类创建每个类的文件.cs。

第三,给每个类文件,根据类中所需要修改的变量,自动添加变量消息和赋值函数。

第四,把消息注册自动的编写到原有的代码的start函数中,这就结束了。

这里有个小的约束:
函数必须有

void Start ()
    { 
    }

必须有,并且最好大括号不能在同一行,也就是说不能写成

void Start ()    { 


因为需要便是这个函数,在里面添加代码;

void Start ()
    {   
           // auto regist code. @cz
           Start4AutoSubscribe(); 
        transf = this.transform;
        //string messName = GETNAME(new { this.movespeed });
        //NotificationManager.Instance.Subscribe(messName, ControlMove_movespeed);
    }


start后面的两行是代码自动添加的。代码中已经做了判断,若添加过

// auto regist code. @cz
           Start4AutoSubscribe(); 


就不会再次添加。

这里还有个保存原有代码的功能呢,就是最原始的你写的项目中代码。根据unity的特性,对点开头的.cs文件视而不见,默认不加入编译中。

这里写图片描述

图2

若有需要可以把原代码给还原了。但是,还是自动svn提交的好。

为什么需要修改源代码呢?
有两个原因:一个是需要注册消息。一个是需要扩展这个需要修改的类,这里会把原来的类前面添加一个关键字partial。

public partial class ControlMove : MonoBehaviour
{


代码中没有加,没有关系,代码会自动运行修改。

你需要做的就是按下AutoModifyClass按钮,等待编译修改。

六、具体使用


消息的处理,也就是这个消息机制之前说过多次。主要用于解耦对象。
这里不过多说明了。

说怎么使用呢?

第一,把带导入的项目中,把prefab文件中的TestConsole拖到场景中即可。

这里写图片描述

图5

第二,把项目中所需要在游戏中动态修改的变量添加属性

 [ModifyAttribute]

修类型不限于public。当前对变量的类型还是做了限制的,只有public。
限制的代码在这里:

var flags = BindingFlags.Instance  | BindingFlags.Public | BindingFlags.Static;


第三,点击生成按钮

添加完毕后,点击生成按钮。

这里写图片描述

图3

第四,所有代码代码就会自动生成,也会修改源工程中的部分需要修改的类的代码。


第五,运行

这里写图片描述

图4

运行,使用tab键或~键都可以调出和关闭控制台窗口。
在输入类,变量,修改值,就可以看到了。

这适用于基础版本的打包测试,编辑器测试,或其他项目,不用为了一个变量来回的打包测试,当然也可以与其他比方说json或xml,数据库等配合使用。

七、下一步需要处理


目前的还需要做的事情:

1.命令行不区分大小写,输入参数现在必须区分大小写,有空格隔开。可能需要处理为不区分大小写的模式。

2.命令行需要丰富,现在控制台命名只有两个,一个是清屏clear,一个是退出控制台exit。

3.控制台命令行输入提示,根据需要提示和回滚之前的输入信息。

4.项目中源代码的回滚功能,防止错误的替换。

八、源码分享


项目地址:

https://github.com/cartzhang/dynamic_parameter_frame


相关图片可下载地址:
https://github.com/cartzhang/dynamic_parameter_frame/tree/master/image


更多控制台的输入项目工程地址和说明,见参考。

九、参考

【1】 https://github.com/cartzhang/UnityConsoleWindow

【2】 http://blog.csdn.net/cartzhang/article/details/49818953

【3】 http://blog.csdn.net/cartzhang/article/details/49884507

<think>我们正在讨论Unity游戏内事件框架的搭建。根据用户提供的引用,特别是引用[4]中提到了使用ScriptableObject和泛型来创建事件系统。这种事件框架的核心是利用Unity的ScriptableObject创建可共享的事件资产,以及使用UnityAction来实现事件的注册和触发。 关键步骤: 1.创建一个泛型的事件基类(BaseEventSO),继承自ScriptableObject。 2.定义UnityAction<T>作为事件触发时的回调。 3.提供RaiseEvent方法来触发事件,并传递数据。 4.创建事件监听者(EventListener),用于订阅事件并响应。 此外,通常还需要一个事件通道(Event Channel)的概念,作为事件资产(ScriptableObject实例)的容器,以便不同对象之间通过事件资产进行通信。 下面,我们将详细说明如何搭建这样一个事件框架。 ### 步骤1:创建事件基类(BaseEventSO) 这是一个泛型基类,用于定义事件资产。注意,我们不直接使用这个基类,而是创建具体的非泛型子类(例如:VoidEventSO, IntEventSO等)。 根据引用[4]中的代码,我们可以这样写: ```csharp using UnityEngine; using UnityEngine.Events; /// <summary> /// 事件SO基类 /// </summary> public abstract class BaseEventSO<T> : ScriptableObject { [Header("事件描述")] [TextArea] public string description; public UnityAction<T> OnEventRaised; // 可选:记录最后一个发送者 public string lastSender; /// <summary> /// 广播事件 /// </summary> /// <param name="value">要广播的值</param> /// <param name="sender">广播发送者(可选)</param> public void RaiseEvent(T value, object sender = null) { OnEventRaised?.Invoke(value); if (sender != null) lastSender = sender.ToString(); } } ``` ### 步骤2:创建具体事件类型 例如,我们可以创建一个传递整数的事件类型: ```csharp [CreateAssetMenu(menuName = "Events/IntEvent")] public class IntEventSO : BaseEventSO<int> { // 这里可以添加Int事件特有的内容,或者留空 } ``` 再例如,一个不需要传递数据的事件(Void事件): ```csharp [CreateAssetMenu(menuName = "Events/VoidEvent")] public class VoidEventSO : BaseEventSO { // 但是注意:BaseEventSO是泛型类,所以我们需要一个非泛型的版本用于Void事件? // 因此,我们可以创建一个非泛型的基类,或者为Void事件单独创建一个类。 } ``` 但是,由于Void事件不需要传递数据,我们可以这样处理: 方案一:创建另一个非泛型的基类(BaseEventSO)的派生类。 或者方案二:使用BaseEventSO<Void>,但是我们无法在Unity中序列化Void类型。因此,我们单独创建一个VoidEventSO。 这里我们采用方案二,但需要调整:我们可以创建一个非泛型的基类,然后让BaseEventSO<T>继承自它,这样VoidEventSO就可以直接继承非泛型基类。 调整如下: 首先,创建一个非泛型基类(BaseEventSO): ```csharp public abstract class BaseEventSO : ScriptableObject { [Header("事件描述")] [TextArea] public string description; public string lastSender; } ``` 然后,让泛型类继承它: ```csharp public abstract class BaseEventSO<T> : BaseEventSO { public UnityAction<T> OnEventRaised; public void RaiseEvent(T value, object sender = null) { OnEventRaised?.Invoke(value); if (sender != null) lastSender = sender.ToString(); } } ``` 接着,创建VoidEventSO: ```csharp [CreateAssetMenu(menuName = "Events/VoidEvent")] public class VoidEventSO : BaseEventSO { public UnityAction OnEventRaised; public void RaiseEvent(object sender = null) { OnEventRaised?.Invoke(); if (sender != null) lastSender = sender.ToString(); } } ``` ### 步骤3:创建事件监听者(EventListener) 事件监听者需要订阅事件并在事件发生时执行响应方法。 我们可以创建一个通用的监听者组件: ```csharp using UnityEngine; public class EventListener<T> : MonoBehaviour { [SerializeField] private BaseEventSO<T> eventSO; // 要监听的事件 [SerializeField] private UnityEvent<T> response; // 当事件发生时触发的响应 private void OnEnable() { if (eventSO != null) eventSO.OnEventRaised += OnEventRaised; } private void OnDisable() { if (eventSO != null) eventSO.OnEventRaised -= OnEventRaised; } private void OnEventRaised(T value) { response.Invoke(value); } } ``` 对于Void事件,我们需要单独创建一个监听者: ```csharp public class VoidEventListener : MonoBehaviour { [SerializeField] private VoidEventSO eventSO; [SerializeField] private UnityEvent response; private void OnEnable() { if (eventSO != null) eventSO.OnEventRaised += OnEventRaised; } private void OnDisable() { if (eventSO != null) eventSO.OnEventRaised -= OnEventRaised; } private void OnEventRaised() { response.Invoke(); } } ``` ### 步骤4:创建事件发送者(Event Raiser) 实际上,任何脚本都可以通过持有事件资产的引用,然后调用其RaiseEvent方法来触发事件。例如: ```csharp public class ExampleEventRaiser : MonoBehaviour { public IntEventSO intEvent; // 引用一个IntEventSO资产 void Update() { if (Input.GetKeyDown(KeyCode.Space)) { intEvent.RaiseEvent(10, this); // 触发事件,传递整数10 } } } ``` ### 步骤5:在Unity编辑器中配置 1. 创建事件资产:在Project窗口中右键 -> Create -> Events -> IntEvent(或其他事件类型)。 2. 在需要发送事件的脚本中,将创建的事件资产拖拽到public字段中。 3. 在需要响应事件的游戏对象上,添加EventListener组件,然后: - 将事件资产拖到eventSO字段 - 在response中绑定需要执行的函数(可以是UnityEvent支持的任意函数,包括公开的方法、动态函数等) 这样,当发送者调用RaiseEvent时,所有监听了该事件的监听者就会触发其响应。 ### 优势 1. **解耦合**:事件发送者和监听者不需要相互引用,只需通过事件资产通信。 2. **复用性**:一个事件资产可以被多个发送者和监听者使用。 3. **可视化**:在编辑器中可配置,便于非程序员使用。 注意:这种方法在大型项目中可能会创建大量的事件资产,所以需要合理组织。 引用[4]中提到的框架可以作为中小型项目的良好起点。 下面,我们将通过一个简单的例子来演示如何使用: 1. 创建一个IntEventSO资产,命名为“MyIntEvent”。 2. 创建一个Cube,添加一个脚本(EventRaiser),引用MyIntEvent,并在按下空格时触发事件(传递一个整数)。 3. 创建一个Sphere,添加一个IntEventListener,将MyIntEvent拖到eventSO字段,在response中添加一个方法(例如改变位置,改变颜色等),绑定到该事件。 这样,当按下空格时,Cube触发事件,Sphere就会响应。 根据以上步骤,可以在Unity中搭建一个基本的事件框架。 §§相关问题§§ 1. 事件框架中如何避免内存泄漏? 2. 如何在事件框架中传递多个参数? 3. 除了使用ScriptableObject,还有哪些实现事件系统的方式? </think>### 如何在Unity中搭建游戏内事件框架Unity中搭建游戏内事件框架的核心目标是实现**模块解耦**和**高效通信**。以下是基于ScriptableObject的泛型事件框架实现方法(参考引用[4]的核心思路): #### 1. 创建事件基类(核心组件) ```csharp using UnityEngine; using UnityEngine.Events; public abstract class BaseEventSO<T> : ScriptableObject { [TextArea] public string description; public UnityAction<T> OnEventRaised; public void RaiseEvent(T value, object sender = null) { OnEventRaised?.Invoke(value); } } ``` #### 2. 实现具体事件类型(示例) ```csharp [CreateAssetMenu(menuName = "Events/IntEvent")] public class IntEventSO : BaseEventSO<int> { } [CreateAssetMenu(menuName = "Events/StringEvent")] public class StringEventSO : BaseEventSO<string> { } ``` #### 3. 创建事件监听器 ```csharp public class EventListener<T> : MonoBehaviour { [SerializeField] BaseEventSO<T> eventSO; [SerializeField] UnityEvent<T> response; void OnEnable() => eventSO.OnEventRaised += TriggerResponse; void OnDisable() => eventSO.OnEventRaised -= TriggerResponse; void TriggerResponse(T value) => response.Invoke(value); } ``` #### 4. 实现事件触发(示例场景) ```csharp // 玩家受伤时触发事件 public class PlayerHealth : MonoBehaviour { [SerializeField] IntEventSO onPlayerHurt; void TakeDamage(int damage) { onPlayerHurt.RaiseEvent(damage, this); } } // UI响应伤害事件 public class HealthUI : MonoBehaviour { [SerializeField] IntEventSO onPlayerHurt; void OnEnable() => onPlayerHurt.OnEventRaised += UpdateHealthBar; void OnDisable() => onPlayerHurt.OnEventRaised -= UpdateHealthBar; void UpdateHealthBar(int damage) { // 更新血条逻辑 } } ``` ### 框架优势 1. **解耦设计**:组件间通过事件通信,无需直接引用[^4] 2. **可视化配置**:在Inspector面板拖拽关联事件资产 3. **类型安全**:泛型确保数据类型一致性 4. **跨场景通信**:ScriptableObject资产全局可用 5. **易扩展性**:添加新事件只需创建SO衍生类 ### 最佳实践建议 1. **事件分类管理**: - 创建`Events`文件夹存放所有事件SO - 按功能分类:`InputEvents`、`UIEvents`、`GameplayEvents` 2. **错误处理**: ```csharp void RaiseEvent(T value) { if(OnEventRaised == null) Debug.LogWarning($"事件 {name} 无监听者"); else OnEventRaised.Invoke(value); } ``` 3. **性能优化**: - 对高频事件(如每帧触发)采用队列机制 - 使用`[SerializeField]`替代`public`修饰事件字段 > **应用场景示例**:当玩家拾取道具时,通过`ItemPickedEvent`同时触发:①UI更新 ②成就系统统计 ③音效播放,三者完全解耦[^1][^4]
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值