【Unity】通用GM QA工具 运行时数值修改 命令行 测试工具

GM工具使用:

 

GM工具通常用于游戏运行时修改数值(加钱/血量)、解锁关卡等,用于快速无死角测试游戏。一个通用型GM工具对于游戏项目是非常实用且必要的,但通用不能向易用妥协,纯命令行GM门槛太高,对QA不友好。

这类运行时命令行工具实现原理很简单,主要是通过给变量或方法添加Attribute标识,然后通过反射获取被标记的变量或方法,命令触发通过反射为变量赋值或Invoke方法。

此类工具免费或付费的已经泛滥了,不推荐浪费时间重复造轮子。

1. 免费开源的Log显示工具,也嵌入了命令行功能。由于GF有更好用的Debuger窗口了,所以没选择它:https://github.com/yasirkula/UnityIngameDebugConsole

2.  Quantum Console, 收费,AssetStore上好评最多,但强行绑定了一个UGUI界面,无解耦。这里我是想直接扩展进GF Debuger窗口,方便使用,因此需要修改插件源码:Quantum Console | Utilities Tools | Unity Asset Store

 感兴趣的话直接AssetStore搜“command console”,免费的也有很多。

我就不浪费时间筛选,直接选择购买好评较多的Quantum Console进行整改。

Quantum Console用法:

Quantum C默认只对继承MonoBehavior的脚本有效,应该是因为需要反射获取所有类型速度太慢,初始化时会卡顿。

对于继承自MonoBehavior的脚本直接通过以下Attribute标记命令即可:

1. 命令行前缀,CommandPrefix("GM."):

相当于给命令行分组,比如把所有命令行标记个前缀叫“GM.”, 那么输入"GM"时所有GM开头的命令都会在列表中显示出来。

[CommandPrefix("GM.玩家.")]
public class PlayerEntity
{
}

2. 把变量或方法作为命令行,Command("命令名字", "命令用法说明"):

[Command("移动速度", "float类型,默认值10")]
private float moveSpeed = 10f;
[Command("添加敌人", "参数int,创建敌人个数")]
internal void AddEnemies(int v)

 对于非MonoBehavior脚本需要手动调用注册命令接口,将该类型添加到需要反射扫描的名单里:

1. QuantumRegistry.RegisterObject()和QuantumRegistry.DeregisterObject()注册或取消注册,然后通过Command("命令名字", "命令描述", MonoTargetType.Registry)添加命令:

public class PlayerDataModel : DataModelBase
{protected override void OnCreate(RefParams userdata){QuantumRegistry.RegisterObject(this);}protected override void OnRelease(){QuantumRegistry.DeregisterObject(this);}[Command("金币", "玩家金币数量", MonoTargetType.Registry)]public int Coins;
}

将Quantum C扩展进GF:

由于GF解耦做得非常好了,我们只需要自定义类实现GameFramework.Debugger.IDebuggerWindow接口就可以写自己的GUI界面和功能了。

1. 扩展Debuger菜单栏,编写GM工具交互界面:

using System.Collections.Generic;
using UnityEngine;
using GameFramework;
using GameFramework.Debugger;
using System;
using Cysharp.Threading.Tasks;
using GM.Utilities;
using System.Threading.Tasks;
using System.Reflection;
using System.Linq;
namespace GM
{public class GMConsoleWindow : IDebuggerWindow{const string LogCommand = "{0}";const string LogSuccess = "<color=#2BD988>{0}</color>";const string LogFailed = "<color=#F22E2E>{0}</color>";const string InputFieldCtrlID = "Input";private int m_MaxLine = 100;private int m_MaxRecordInputHistory = 30;private Queue<GMLogNode> m_LogNodes;private LinkedList<string> m_InputHistoryList;private LinkedListNode<string> m_CurrentHistory = null;string m_InputText;string m_PreInputText;bool m_InputFocused;bool m_InputChanged;Vector2 m_ScrollPosition = Vector2.zero;Vector2 m_FilterScrollPosition = Vector2.zero;SuggestionStack m_CommandsFilter;SuggestorOptions m_FilterOptions;Rect inputRect = default;bool m_LogAppend;bool m_MoveCursorToEnd;GUIStyle m_CommandsFilterBtStyle;private readonly Type m_VoidTaskType = typeof(Task<>).MakeGenericType(Type.GetType("System.Threading.Tasks.VoidTaskResult"));private List<System.Threading.Tasks.Task> m_CurrentTasks;private List<IEnumerator<ICommandAction>> m_CurrentActions;public void Initialize(params object[] args){if (!QuantumConsoleProcessor.TableGenerated){QuantumConsoleProcessor.GenerateCommandTable(true);}m_InputHistoryList = new LinkedList<string>();m_LogNodes = new Queue<GMLogNode>();m_CurrentTasks = new List<System.Threading.Tasks.Task>();m_CurrentActions = new List<IEnumerator<ICommandAction>>();m_CommandsFilter = new SuggestionStack();m_FilterOptions = new SuggestorOptions(){CaseSensitive = false,CollapseOverloads = true,Fuzzy = true,};}public void OnDraw(){if (m_CommandsFilterBtStyle == null){m_CommandsFilterBtStyle = new GUIStyle(GUI.skin.button){alignment = TextAnchor.MiddleLeft};}GUILayout.BeginVertical();{m_ScrollPosition = GUILayout.BeginScrollView(m_ScrollPosition, "box");{foreach (var logNode in m_LogNodes){GUILayout.Label(logNode.LogMessage);}GUILayout.EndScrollView();}if (m_LogAppend){m_LogAppend = false;m_ScrollPosition = new Vector2(0, float.MaxValue);}GUILayout.BeginHorizontal();{GUI.enabled = QuantumConsoleProcessor.TableGenerated;GUI.SetNextControlName(InputFieldCtrlID);m_InputText = GUILayout.TextField(m_InputText);if (Event.current.type == EventType.Repaint){inputRect = GUILayoutUtility.GetLastRect();if (m_MoveCursorToEnd){m_MoveCursorToEnd = false;MoveInputCursorToEnd();}}m_InputFocused = (GUI.GetNameOfFocusedControl() == InputFieldCtrlID);m_InputChanged = m_InputText != m_PreInputText;if (m_InputChanged){m_PreInputText = m_InputText;m_CommandsFilter.UpdateStack(m_InputText, m_FilterOptions);}if (GUILayout.Button("Execute", GUILayout.Width(60))){ExecuteCommand(m_InputText);}if (GUILayout.Button("Clear", GUILayout.Width(60))){ClearLogs();}GUILayout.EndHorizontal();}GUILayout.EndVertical();if (m_InputFocused && m_CommandsFilter.TopmostSuggestionSet != null){if (Event.current.type == EventType.Repaint){float maxHeight = GUILayoutUtility.GetLastRect().height - inputRect.height - 5f;inputRect.height = Mathf.Clamp(m_CommandsFilter.TopmostSuggestionSet.Suggestions.Count * 30, maxHeight * 0.5f, maxHeight);inputRect.position -= Vector2.up * (inputRect.height + 5f);}if (m_InputChanged){m_FilterScrollPosition = Vector2.zero;}GUILayout.BeginArea(inputRect);m_FilterScrollPosition = GUILayout.BeginScrollView(m_FilterScrollPosition, "box", GUILayout.ExpandWidth(true), GUILayout.ExpandHeight(true));{GUILayout.BeginVertical(GUILayout.ExpandHeight(true));{foreach (var item in m_CommandsFilter.TopmostSuggestionSet.Suggestions){if (GUILayout.Button(item.FullSignature, m_CommandsFilterBtStyle)){m_MoveCursorToEnd = true;var fragments = m_InputText.Split(' ');if (fragments.Length >= 2){m_InputText = string.Empty;for (int i = 0; i < fragments.Length - 1; i++){m_InputText = Utility.Text.Format("{0}{1}{2}", m_InputText, i == 0 ? string.Empty : " ", fragments[i]);}m_InputText = Utility.Text.Format("{0} {1}", m_InputText, item.PrimarySignature);}else{m_InputText = item.PrimarySignature;}}}GUILayout.EndVertical();}GUILayout.EndScrollView();}GUILayout.EndArea();}}}/// <summary>/// 输入框游标移动到尾部/// </summary>private void MoveInputCursorToEnd(){GUI.FocusControl(InputFieldCtrlID);// 获取当前TextEditorTextEditor editor = (TextEditor)GUIUtility.GetStateObject(typeof(TextEditor), GUIUtility.keyboardControl);if (editor != null){editor.cursorIndex = m_InputText.Length;editor.selectIndex = m_InputText.Length;}}public void OnEnter(){QuantumRegistry.RegisterObject<GMConsoleWindow>(this);}public void OnLeave(){QuantumRegistry.DeregisterObject<GMConsoleWindow>(this);}public void OnUpdate(float elapseSeconds, float realElapseSeconds){if (m_InputFocused){if (Input.GetKeyDown(KeyCode.Return) || Input.GetKeyDown(KeyCode.KeypadEnter))ExecuteCommand(m_InputText);if (Input.GetKeyDown(KeyCode.DownArrow)){SelectInputHistory(false);}else if (Input.GetKeyDown(KeyCode.UpArrow)){SelectInputHistory(true);}}TasksUpdate();ActionsUpdate();}public void Shutdown(){}private void SelectInputHistory(bool upOrdown){if (m_InputHistoryList.Count == 0) return;m_MoveCursorToEnd = true;if (upOrdown){if (m_CurrentHistory == null || m_CurrentHistory.Previous == null){m_InputText = m_InputHistoryList.Last.Value;m_CurrentHistory = m_InputHistoryList.Last;return;}m_InputText = m_CurrentHistory.Previous.Value;m_CurrentHistory = m_CurrentHistory.Previous;}else{if (m_CurrentHistory == null || m_CurrentHistory.Next == null){m_InputText = m_InputHistoryList.First.Value;m_CurrentHistory = m_InputHistoryList.First;return;}m_InputText = m_CurrentHistory.Next.Value;m_CurrentHistory = m_CurrentHistory.Next;}}private void AppendLog(GMLogType logType, string logMessage){m_LogNodes.Enqueue(GMLogNode.Create(logType, logMessage));while (m_LogNodes.Count > m_MaxLine){ReferencePool.Release(m_LogNodes.Dequeue());}m_LogAppend = true;}[Command("clear", "清空GM日志", MonoTargetType.Registry)]private void ClearLogs(){m_LogNodes.Clear();m_ScrollPosition = Vector2.zero;}private void ExecuteCommand(string cmd, bool recordHistory = true){if (string.IsNullOrWhiteSpace(cmd)) return;if (recordHistory) RecordInputHistory(cmd);AppendLog(GMLogType.Command, cmd);m_InputText = string.Empty;try{var commandResult = QuantumConsoleProcessor.InvokeCommand(cmd);if (commandResult != null){if (commandResult is IEnumerator<ICommandAction> enumeratorTp){m_CurrentActions.Add(enumeratorTp);ActionsUpdate();}else if (commandResult is IEnumerable<ICommandAction> enumerableTp){m_CurrentActions.Add(enumerableTp.GetEnumerator());ActionsUpdate();}else if (commandResult is UniTask task){m_CurrentTasks.Add(task.AsTask());}else if (commandResult.GetType().Name == "UniTask`1"){var asTaskGenericMethod = typeof(UniTaskExtensions).GetMethods(BindingFlags.Public | BindingFlags.Static).FirstOrDefault(item => item.Name == "AsTask" && item.IsGenericMethod);Type uniTaskType = commandResult.GetType();Type genericArgument = uniTaskType.GetGenericArguments()[0];MethodInfo genericMethod = asTaskGenericMethod.MakeGenericMethod(genericArgument);Task taskT = (Task)genericMethod.Invoke(null, new object[] { commandResult });m_CurrentTasks.Add(taskT);}else{var resultType = commandResult.GetType();if (resultType == typeof(string) || resultType.IsPrimitive){AppendLog(GMLogType.Success, commandResult.ToString());}else{AppendLog(GMLogType.Success, Utility.Json.ToJson(commandResult));}}}}catch (System.Reflection.TargetInvocationException e){AppendLog(GMLogType.Failed, e.Message);}catch (Exception e){AppendLog(GMLogType.Failed, e.Message);}}private void RecordInputHistory(string cmd){if (m_InputHistoryList.Count > 0 && m_InputHistoryList.First.Value == cmd) return;m_InputHistoryList.AddFirst(cmd);m_CurrentHistory = m_InputHistoryList.Last;while (m_InputHistoryList.Count > m_MaxRecordInputHistory){m_InputHistoryList.RemoveLast();}}private void TasksUpdate(){for (int i = m_CurrentTasks.Count - 1; i >= 0; i--){if (m_CurrentTasks[i].IsCompleted){if (m_CurrentTasks[i].IsFaulted){foreach (Exception e in m_CurrentTasks[i].Exception.InnerExceptions){AppendLog(GMLogType.Failed, e.Message);}}else{Type taskType = m_CurrentTasks[i].GetType();if (taskType.IsGenericTypeOf(typeof(Task<>)) && !m_VoidTaskType.IsAssignableFrom(taskType)){System.Reflection.PropertyInfo resultProperty = m_CurrentTasks[i].GetType().GetProperty("Result");object result = resultProperty.GetValue(m_CurrentTasks[i]);string log = Utility.Json.ToJson(result);AppendLog(GMLogType.Success, log);}}m_CurrentTasks.RemoveAt(i);}}}private void ActionsUpdate(){for (int i = m_CurrentActions.Count - 1; i >= 0; i--){IEnumerator<ICommandAction> action = m_CurrentActions[i];try{if (action.Execute() != ActionState.Running){m_CurrentActions.RemoveAt(i);}}catch (Exception e){m_CurrentActions.RemoveAt(i);AppendLog(GMLogType.Failed, e.Message);break;}}}private enum GMLogType{Command,Success,Failed}/// <summary>/// 日志记录结点。/// </summary>private sealed class GMLogNode : IReference{private GMLogType m_LogType;private string m_LogMessage;/// <summary>/// 初始化日志记录结点的新实例。/// </summary>public GMLogNode(){m_LogType = GMLogType.Failed;m_LogMessage = null;}/// <summary>/// 获取日志类型。/// </summary>public GMLogType LogType{get{return m_LogType;}}/// <summary>/// 获取日志内容。/// </summary>public string LogMessage{get{return m_LogMessage;}}/// <summary>/// 创建日志记录结点。/// </summary>/// <param name="logType">日志类型。</param>/// <param name="logMessage">日志内容。</param>/// <returns>创建的日志记录结点。</returns>public static GMLogNode Create(GMLogType logType, string logMessage){GMLogNode logNode = ReferencePool.Acquire<GMLogNode>();logNode.m_LogType = logType;switch (logType){case GMLogType.Success:logNode.m_LogMessage = Utility.Text.Format(LogSuccess, logMessage);break;case GMLogType.Failed:logNode.m_LogMessage = Utility.Text.Format(LogFailed, logMessage);break;default:logNode.m_LogMessage = Utility.Text.Format(LogCommand, logMessage);break;}return logNode;}/// <summary>/// 清理日志记录结点。/// </summary>public void Clear(){m_LogType = GMLogType.Failed;m_LogMessage = null;}}}
}

2. 将自定义的GM工具界面注册进GF Debuger窗口:

GF.Debugger.RegisterDebuggerWindow("GM", new GM.GMConsoleWindow());

效果:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/52331.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

进程的创建、终止

目录 前言1. 进程创建2. 进程终止3. exit && _exit 的异同3.1 相同点3.2 不同点 前言 紧接着进程地址空间之后&#xff0c;我们这篇文章开始谈论进程控制相关的内容&#xff0c;其中包括进程是如何创建的&#xff0c;进程终止的几种情况&#xff0c;以及进程异常终止的…

数学建模学习(115):主成分分析(PCA)与Python实践

文章目录 一.主成分分析简介1.1 数学背景与维度诅咒1.2 PCA的定义与应用二.协方差矩阵——特征值和特征向量三.如何为数据集选择主成分数量四.特征提取方法五.LDA——与PCA的区别六.PCA的应用七.PCA在异常检测中的应用八.总结一.主成分分析简介 1.1 数学背景与维度诅咒 主成成…

TOP10漏洞原理

## 本人为学习网安不久的新人&#xff0c;记一次学习笔记&#xff0c;有缺陷或者表述不对的地方欢迎大家指出&#xff0c;感谢&#xff01; ## 1、sql注入&#xff1a;web应用程序对用户输入的数据没有进行过滤&#xff0c;或者过滤不严&#xff0c;就把sql语句拼接进数据库…

Mac电脑遇到DNS解析失败,ip可以访问,域名无法访问

当Mac电脑遇到DNS解析失败的问题时&#xff0c;可以尝试以下几个解决方法‌&#xff1a; 1.检查网络连接‌&#xff1a;确保Mac已连接到可用的网络&#xff0c;并且网络连接正常。可以尝试重新连接Wi-Fi或使用有线连接来排除网络问题。 2.清除DNS缓存‌&#xff1a;打开终端应…

docker容器基本命令、docker进入容器的指令、容器的备份、镜像底层原理、使用commit命令制造镜像、将镜像推送到阿里云镜像仓库与私服仓库

除了exit 还有 ctrlpq exit退出停止 ctrlpq 退出不停止 将本地镜像推到阿里云 登入阿里云 容器镜像服务 实力列表 镜像仓库 创建镜像仓库 安装里面步骤来 这里192.168.10.145这部分用自己ifconfig地址

【Android 远程数据库操作】

按正常情况下&#xff0c;前端不应该直接进行远程数据库操作&#xff0c;这不是一个明智的方式&#xff0c;应该是后端提供对应接口来处理&#xff0c;奈何公司各方面原因需要前端这样做。 对此&#xff0c;我对远程数据库操作做了总结&#xff0c;便于自己复盘&#xff0c;同…

python绘制爱心代码

效果展示 完整代码 Python中绘制爱心的代码可以通过多种方式实现&#xff0c;高级的爱心代码通常指的是使用较复杂的算法或者图形库来生成更加精致的爱心图形。下面是一个使用Python的Turtle模块来绘制爱心的示例代码&#xff1a; import turtledef draw_love():turtle.speed…

[Other]-安装ruby、ascli、ascp

最近新接到这样一个需求&#xff0c;将生物原始数据上传到某中心&#xff0c;其中用到ascp命令&#xff0c;阴差阳错的装了ruby、ascli&#xff0c;这里就都一并介绍下安装方式&#xff0c;由于服务器老旧默认安装时ruby2.0&#xff0c;又 升级到2.7等引发的一系列问题&#xf…

XSS-DOM

文章目录 源码SVG标签Dom-Clobbringtostring 源码 <script>const data decodeURIComponent(location.hash.substr(1));;const root document.createElement(div);root.innerHTML data;// 这里模拟了XSS过滤的过程&#xff0c;方法是移除所有属性&#xff0c;sanitize…

AI工具革新:国内外设计艺术的融合

在人工智能的浪潮中&#xff0c;全球的创新者和开发者们推出了一系列令人惊叹的工具&#xff0c;它们正以前所未有的速度改变着我们的工作、学习和生活方式。从图像生成到语言处理&#xff0c;从数据分析到自动化设计&#xff0c;AI 作图工具展示了其强大的能力&#xff0c;帮助…

DRF——Filter条件搜索模块

文章目录 条件搜索自定义Filter第三方Filter内置Filter 条件搜索 如果某个API需要传递一些条件进行搜索&#xff0c;其实就在是URL后面通过GET传参即可&#xff0c;例如&#xff1a; /api/users?age19&category12在drf中也有相应组件可以支持条件搜索。 自定义Filter …

面试题详解

前言&#xff1a;这一期我们专门来巩固所学知识&#xff0c;同时见识一些面试题。对知识做出一个总结。 1 不创建临时变量交换两个整数 . 第一种方法 #include<stdio.h> int main() {int a 0;int b 0;scanf("%d %d", &a, &b);printf("交换前…

神经网络算法 - 一文搞懂BERT(基于Transformer的双向编码器)

本文将从BERT的本质、BERT的原理、BERT的应用三个方面&#xff0c;带您一文搞懂Bidirectional Encoder Representations from Transformers | BERT。 Google BERT BERT架构&#xff1a; 一种基于多层Transformer编码器的预训练语言模型&#xff0c;通过结合Tokenization、多种E…

Java基于数据库、乐观锁、悲观锁、Redis、Zookeeper分布式锁的简单案例实现(保姆级教程)

1. 分布式锁的定义 分布式锁是一种在分布式系统中用来协调多个进程或线程对共享资源进行访问的机制。它确保在分布式环境下&#xff0c;多个节点&#xff08;如不同的服务器或进程&#xff09;不会同时访问同一个共享资源&#xff0c;从而避免数据不一致、资源竞争等问题。 2…

等保测评服务的业务连续性规划:确保信息安全服务的韧性

在当前的数字化转型浪潮中&#xff0c;信息安全已成为企业运营的关键一环。等保测评服务作为信息安全合规的重要组成部分&#xff0c;其业务连续性规划对于保障服务的稳定性和客户信息资产的安全至关重要。本文将探讨等保测评服务的业务连续性规划策略&#xff0c;旨在构建一个…

树状数组算法

文章目录 树状数组是什么树状数组与线段树的区别与联系树状数组讲解点修&#xff0c;区查&#xff0c;讲解及模板点查&#xff0c;区修讲解及模板 树状数组是什么 树状数组是一种数据结构&#xff0c;提供O(logn)时间内的单点修改和区间求和操作&#xff0c;比线段树有更优的常…

MD编辑器学习笔记

MD编辑器学习笔记 目录标题文本样式列表图片链接代码片数学公式表格部分总结 目录 目录是使用“[TOC](目录&#xff09;”&#xff0c;记住别忘了加上&#xff08;&#xff09;标题 使用#来确定标题&#xff0c;几个#就是几级标题。记住#后面要加上空格文本样式 tips: 在写正…

物流抓取机器人整体设计方案

一、功能简介 1、运行环境&#xff1a;巡线行驶&#xff08;7路数字循迹&#xff0c;麦克纳姆轮车底盘&#xff09; 2、目标识别&#xff1a;颜色识别&#xff08;Maix-II Dock 视觉模块&#xff09; 3、目标定位&#xff1a;视觉测距&#xff08;Maix-II Dock 视觉模块&#x…

VS实⽤调试技巧(附调试例题)

&#x1f381;&#x1f381;创作不易&#xff0c;关注作者不迷路&#x1f380;&#x1f380; VS实⽤调试技巧&#xff08;附调试例题&#xff09; 前言一、什么是bug&#xff1f;二、什么是调试&#xff08;debug&#xff09;&#xff1f;三、Debug和Release四、VS调试快捷键五…

上书房信息咨询:商业项目调研方法有哪些

商业项目调研是为了了解市场需求、竞争情况和目标受众&#xff0c;从而制定合适的商业策略和项目规划。下面是一些常用的商业项目调研方法&#xff1a; 1、市场调查和问卷调查&#xff1a;通过设计和分发问卷&#xff0c;收集潜在顾客和目标市场的意见、偏好和需求。这可以帮助…