Unity中的作弊面板

Tulenber 1 May, 2020 ⸱ Intermediate ⸱ 8 min ⸱ 2019.3.11f1 ⸱

奇怪的是,作弊是游戏的重要组成部分,这极大地促进了游戏的发展。

Konami代码可能是视频游戏历史上最著名的作弊代码。它是由桥本一久(Kazuhisa Hashimoto)在将Gradius游戏移植到NES控制台时创建的。由于考虑到游戏太难,他添加了代码。使用控制器输入序列后,播放器会收到所有可用的通电信息。后来,它在数百种游戏中实现了多种用途。

在您的新游戏中,添加第一个机制后,您会得到类似于Gradius的情况。她的测试将花费相当长的时间。举例来说,如果您的游戏包含数十个关卡,那么为了测试52级,您不太可能会先浏览整个游戏,以发现地图上的某些元素位于地图的左侧5像素处。这个地方。因此,在游戏中添加作弊就成为解决此类问题的常见做法。它有助于偷工减料,并为检查力学或其他游戏元素创造必要条件。

作弊面板

如果不十分重视这一部分,则作弊的嵌入将从简单的机制开始,例如,通过单击带有生命的图标来完全恢复生命,或者在按“ Ctrl + M”时添加一百个硬币。随后,这转化为以下事实:记住所有这些非显而易见的组合和位置将非常棘手,并且项目中的新手根本不会了解它们。在组装释放组件时,清洁和检查此类位置将更具挑战性。

解决这个问题的一个很好的方法是作弊板,它已成为这类技工的标准。它们提供了通知游戏状态并对其进行管理的功能,而无需与界面进行常规交互。而您所需要的只是一堆小东西,例如使它们便宜地开发,可以轻松地从最终组装中消除,以及很好地集成到游戏机制中。

《名侦探柯南》中的作弊面板:
Conan admin panel

因此,要创建作弊面板,您需要这些元素:

  1. UI工具
  2. 面板外观逻辑
  3. 总装排除机制

Immediate Mode GUI (IMGUI)

第一项要求的答案将是在Unity中创建服务接口的主要工具,命名为Imme1diate Mo1de GUI(IMGUI。用于为Unity编辑器本身创建自定义组件的机制相同,我们在我们的以前的文章有据可查的包。熟悉的功能及其通过自定义编辑器工具的创建极大地方便了您的生活。

面板外观逻辑

鉴于面板不应该与主UI系统进行交互,因为这不是最明显的行为,并且会影响产品的最终组装,因此此要求成为一项有趣的任务。在带有键盘的设备上,最明显的方法是添加组合键以显示面板。但是,在移动设备上,该解决方案无法立即引起人们的注意。最简单的选项之一是添加一个区域,以跟踪长按或轻击屏幕。

条件编译

根据项目规模和团队的工作,项目可能处于不同的开发级别。 在初始级别,注释代码部分似乎是一种将其从最终汇编中排除的可接受方法。 当然,这是非常危险的,丑陋的,耗时的。 因此,如果进行任何类型的生产,都不要这样做。

社区中一个有趣的建议是将脚本添加到Editor文件夹中。 可以通过Type.GetType(“type_name”)方法检查可用性,而最终程序集将不包括它。当然,此机制是为其他任务而设计的,并不提供在设备上测试游戏的机会。仍然是最简单,最快的方法。

从作弊中排除作弊的主要方法仍然是使用预处理程序符号进行良好的旧条件编译。有两种添加方式:

  • 取决于平台-通过项目设置 Edit > Project Settings... > Other Settings > Scripting Define Symbols
  • 全局-通过使用参数-define:CUSTOM_NAMEmcs.rsp/csc.rsp文件(取决于编译器)添加到Assets文件夹中

优先方法是通过项目设置。 其最大的缺点是将所有值复制到每个使用的平台。如果我们谈论自动化的可能性,则有必要在项目中添加一个静态方法,该方法将在组装过程中通过参数-executeMethod传递,并在必要时进行传递。 通过调用PlayerSettings.SetScriptingDefineSymbolsForGroup (BuildTargetGroup.iOS, “DEBUG_CHEATS”);来添加符号。

全局设置将对除Editor文件夹中包含的所有脚本均有效,并且也是有效的方法。但是,由于mcs.rspcsc.rsp文件可能包含其他编译器配置参数,因此这带来了更大的危险。此外,在自动组装的情况下,可能有必要创建其他工具来更改参数。

基于预处理器的代码异常也可以分为两种方法:

  • 使用指令#if define_name, #elif define_name, #else, #endif.
  • 使用Assembly Definitions

指令是最简单,最实惠的选择,最容易应用于现有代码库。

Asembly Definitions对代码体系结构和各种模式的使用提出了更多要求,因为将库划分为对代码的负责任的方法。但是,它们的使用本身很有价值。它可以极大地简化大型项目的寿命,加快项目的重组,并使测试更加方便,如我们有关的文章中所述单元测试。还有一件值得一提的事情是,对于不同的程序集,您可以使用不同的mcs/csc文件,这可以简化自动化。

测试项目

为了演示作弊小组的创建,我们使用硬币机修工构建了一个小项目。

最初,最多有500个硬币。单击该按钮可使硬币减少一百个。硬币每三秒增加一百。
Conan admin panel

实作

鉴于以上所有内容,该实现将看起来像是用IMGUI编写的面板,通过长按(3秒)将出现在屏幕的左下角,并且代码将使用预处理程序指令#if#endif,我们将通过使用项目设置来启用它们。

类管理机制和UI:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
using System;
using System.Collections;
using UnityEngine;
using UnityEngine.UI;

public class CoinsHandler : MonoBehaviour
{
    // UI元素
    [SerializeField] public Text coinsTitle = null;
    [SerializeField] public Text countdownTitle = null;
    [SerializeField] public Button spendButton = null;

    // 硬币机械师的特征
    public const int MaxCoinsCount = 500;
    private const int SpendCoinsCount = 100;
    private int _coinsCount;

    public int CoinsCount => _coinsCount;

    // 硬币回收计时器
    private readonly TimeSpan _timeToRestore = new TimeSpan(0,0,3);
    private DateTime _restoreCoinsTimeMark;

    public DateTime RestoreCoinsTimeMark => _restoreCoinsTimeMark;

    // 分别重绘UI元素
    private bool _updateUi = true;

    void Start()
    {
        // 设置硬币的初始值
        _coinsCount = MaxCoinsCount;

        // 启动计时器检查硬币
        StartCoroutine(UpdateTimer());

        // 如果启用了DEBUG CHEATS标志,则创建负责作弊的对象
#if DEBUG_CHEATS
        GameObject cheatsHandler = new GameObject {name = "CheatsHandler"};
        cheatsHandler.AddComponent<CheatsHandler>();
#endif
    }

    // 更新用户界面
    private void LateUpdate()
    {
        if (_updateUi)
        {
            _updateUi = false;
            UpdateUi();    
        }
    }

    // 更新用户界面
    private void UpdateUi()
    {
        coinsTitle.text = "Coins: " + _coinsCount;

        // 仅当硬币数量少于最大数量时才显示计时器
        countdownTitle.gameObject.SetActive(_coinsCount < MaxCoinsCount);
        if (_coinsCount < MaxCoinsCount)
        {
            // 硬币的未来时间增加与当前时刻之间的时差
            TimeSpan timeDiff = _restoreCoinsTimeMark - DateTime.UtcNow;
            countdownTitle.text = "Countdown: " + timeDiff.ToString("mm\\:ss");    
        }

        // 当可以减少硬币时该按钮处于活动状态
        spendButton.interactable = CanSpendCoins();
    }

    // UI更新计时器
    IEnumerator UpdateTimer()
    {
        while (true)
        {
            CheckCoinsTimer();
            _updateUi = true;
            yield return new WaitForSecondsRealtime(1);
        }
    }

    // 功能减少硬币
    public void SpendCoins()
    {
        if (CanSpendCoins())
        {
            SetCoinsTo(_coinsCount - SpendCoinsCount);
        }
    }

    // 检查是否有足够的硬币
    public bool CanSpendCoins()
    {
        return _coinsCount >= SpendCoinsCount;
    }

    // 硬币计算功能
    public void GetCoins()
    {
        if (_coinsCount < MaxCoinsCount)
        {
            SetCoinsTo(_coinsCount + SpendCoinsCount);
        }
    }

    // 检查投币计时器的功能
    private void CheckCoinsTimer()
    {
        // 当前时间
        DateTime timeNow = DateTime.UtcNow;
        // 如果当前时间大于充电标记且硬币小于最大时间
        if (timeNow >= _restoreCoinsTimeMark && _coinsCount < MaxCoinsCount)
        {
            GetCoins();
            // 移动硬币增加时间标记
            _restoreCoinsTimeMark += _timeToRestore;
        }
    }

    // 统一功能,可设定硬币数量
    public void SetCoinsTo(int count)
    {
        if (_coinsCount >= MaxCoinsCount)
        {
            DateTime timeNow = DateTime.UtcNow;
            _restoreCoinsTimeMark = timeNow + _timeToRestore;
        }
        
        _coinsCount = count;

        _updateUi = true;
    }
}

作弊控制类:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
#if DEBUG_CHEATS

using System;
using System.Globalization;
using UnityEngine;

public class CheatsHandler : MonoBehaviour
{
    // 备忘面板的矩形
    private readonly Rect _cheatsBoxRect = new Rect (10, 10, Screen.width - 20, 200);
    // 用于输入硬币数量的字符串
    private string _coinsInput = "0";

    // 硬币力学的对象
    private CoinsHandler _coinsHandler = null;

    // 备忘板显示的标志
    private bool _showPanel = false;

    // 面板激活计时器
    private const float TimeToActivate = 3;
    private float _activationTimeCounter = 0;
    
    // 矩形检查激活
    private Rect _checkRect = new Rect(0,0,50,50);
    private bool _buttonPressed = false;

    void Start()
    {
        // 告知作弊的存在
        Debug.LogWarning("Debug cheats enabled");
        _coinsHandler = FindObjectOfType<CoinsHandler>();
    }

    void Update()
    {
        // 增加面板激活区域上的点击计时器
        if (_buttonPressed)
        {
            _activationTimeCounter += Time.deltaTime;
        }

        // 点击足够长的时间,显示面板
        if (_activationTimeCounter >= TimeToActivate)
        {
            _showPanel = true;
        }

        // 显示面板的键盘快捷键
        if (Input.GetKey(KeyCode.LeftAlt) && Input.GetKey(KeyCode.C))
        {
            _showPanel = true;
        }

        // 在面板激活区域中跟踪单击的开始
        if (Input.GetMouseButtonDown(0) && _checkRect.Contains(Input.mousePosition))
        {
            _buttonPressed = true;
        }

        // 重置点击信息
        if (Input.GetMouseButtonUp(0))
        {
            _buttonPressed = false;
            _activationTimeCounter = 0;
        }

        /*
        // 在面板激活区域中跟踪点击的开始
        if (Input.touchCount > 0)
        {
            Touch touch = Input.GetTouch(0);
            if (_checkRect.Contains(touch.position))
            {
                _buttonPressed = true;
            }
        }

        // 重置点击信息
        if (Input.touchCount == 0 && _buttonPressed)
        {
            _buttonPressed = false;
            _activationTimeCounter = 0;
        }*/
    }

    // 渲染备忘面板的主要功能
    private void OnGUI()
    {
        // 未经激活请勿绘制面板
        if (!_showPanel)
        {
            return;
        }

        // 面板的主要对象
        GUI.Box(_cheatsBoxRect, "Cheats panel");

        // 自动布局区域的开始
        GUILayout.BeginArea (new Rect(20, 35, Screen.width - 40, 180));
        // 开始垂直放置元素
        GUILayout.BeginVertical();
        // 显示生命数
        GUILayout.Label("Lives count: " + _coinsHandler.CoinsCount + (_coinsHandler.CoinsCount >= CoinsHandler.MaxCoinsCount ? " (Max)" : ""));
        // 显示当前时间
        GUILayout.Label("Current time: " + DateTime.UtcNow.ToString("G", CultureInfo.InvariantCulture));
        // 计算当前时间与硬币充值时间之间的时差
        TimeSpan timeDiff = _coinsHandler.RestoreCoinsTimeMark - DateTime.UtcNow;
        // 如果应计时间已经过去,请加上减号
        string minus = timeDiff < TimeSpan.Zero ? "-" : "";
        // 显示确切的应计时间以及以分钟和秒为单位的差异
        GUILayout.Label("Restore time: " + _coinsHandler.RestoreCoinsTimeMark.ToString("G", CultureInfo.InvariantCulture) + " (" + minus + timeDiff.ToString("mm\\:ss") + ")");

        // 开始元素的水平放置
        GUILayout.BeginHorizontal();
        // 输入要计入的硬币数量的字段
        string testStr = GUILayout.TextField(_coinsInput);
        // 通过验证更新输入字段中的文本
        if (IsDigitsString(testStr))
        {
            _coinsInput = testStr;
        }

        // 通过验证记入来自输入字段的硬币数量
        if (GUILayout.Button("Set") && Int32.TryParse(_coinsInput, out int lives))
        {
            _coinsHandler.SetCoinsTo(lives);
        }
        // 元素水平放置的末端
        GUILayout.EndHorizontal();

        // 开始元素的水平放置
        GUILayout.BeginHorizontal();
        // 将硬币设为零的按钮
        if (GUILayout.Button("0"))
        {
            _coinsHandler.SetCoinsTo(0);
        }
        // 提取一百个硬币的按钮
        if (GUILayout.Button("-100"))
        {
            _coinsHandler.SpendCoins();
        }
        // 按钮添加一百个硬币
        if (GUILayout.Button("+100"))
        {
            _coinsHandler.GetCoins();
        }
        // 将硬币设置为最大的按钮
        if (GUILayout.Button("Max"))
        {
            _coinsHandler.SetCoinsTo(CoinsHandler.MaxCoinsCount);
        }
        // 元素水平放置的末端
        GUILayout.EndHorizontal();

        // 按钮关闭面板
        if (GUILayout.Button("Close panel"))
        {
            _showPanel = false;
        }

        // 元素垂直放置的末端
        GUILayout.EndVertical();
        // 自动布局区域的末端
        GUILayout.EndArea();
    }

    // 仅验证小数输入的硬币
    private static bool IsDigitsString(string s)
    {
        if (s == null || s == "") 
            return false;
        
        for (int i = 0; i < s.Length; i++) 
            if (s[i] < '0' || s[i] > '9') 
                return false; 
        return true;
    }
}

#endif
Scripting Define Symbol添加到Project Settings
Scripting define symbols

结果

除了长按打开以外,我们还添加了一个组合键。 tap选项已注释,因为尚未测试。
Result

结论

作弊面板是帮助测试游戏的主要内容之一,游戏中引入的系统和机制越复杂,创建此类面板的关注就越多。乍一看,这些面板看起来并不复杂。但是,有时嵌入作弊可能不仅需要精心构建的体系结构,而且甚至需要扩展游戏机制本身,因此这可能不是最简单的解决方案。如果您在此处包括繁殖用于测试和生产的构建的要求,则不仅限于编程,还应包括具有CI设置和所有其他有助于减少体力劳动的dev_ops责任区。另外,新项目仅必须使用Assembly Definition的功能来组织代码和其他功能,但这超出的作弊面板的范围,我们将单独讨论。作弊少!下次见!^_^


如果您喜欢这篇文章,可以为它提供支持



Privacy policyCookie policyTerms of service
Tulenber 2020