功能说明
今天要实现的功能就是老滚5、GTA、P社等游戏里面那个按下某个按键就能开启调试(开挂)的控制台组件。老规矩,直接上实际效果图:
如上图所示,输入输出文本的控制台本质上就是一个游戏中内嵌的命令行。那么,它一定会包含以下几点功能:
- 一个指令输入栏和一个回调输出框;
- 能够通过“上”和“下”按键快速选取已经使用过的指令;
- 有help指令能列出指令清单;
- 有清屏指令;
- 有一个滚动条能浏览历史。
除此以外,为了嵌在游戏当中,并且能与工程解耦,则也包含如下几点功能:
- 通过快捷键快速弹出/关闭;
- 控制面板与指令实现类低耦合。
一、控制台界面设计
先从界面设计来讲起,重点说明Scrollbar的使用:
1. 添加Panel组件
如下图左侧层级面板中显示的那样,先插入一个Panel,命名为“Console Panel”。再在Console Panel中插入一个InputField和一个子Panel,并将子Panel命名为“Output Panel”;
2. 调节InputField和Output Panel的布局
如下图所示,先将InputField的布局模式改为底端扩展,再将Output Panel的布局模式如红框中那样改为全局扩展,并调节Botttom避免与InputField重叠;
3. 添加滚动条
如下图左侧层级面板所示,在Ouput Panel中添加一个Scrollbar和一个Text组件,并将Text命名为“Output Text”;
4. 修改子组件属性
对于Scrollbar而言,如下图所示修改两处内容,让Scrollbar竖向沿父组件右侧扩展。其中,Scrollbar中有几个参数需要说明:
- Direction指的是当某一个物体超出面板的显示范围后,Scrollbar拖动的方向(上到下、下到上、左到右、右到左);
- Value指的是,滑块处于整个Scrollbar的位置,0.5则表示滑块居中;
- Number of Steps表示滑块每次移动的步长;
而对于Output Text,添加一个Content Size Fitter组件,并将Vertical Fit 修改为“Preferred Size”,使文本在垂直方向的宽度自适应,如下图所示:
5. Ouput Panel绑定子组件
如下图所示,在Output Panel中,添加Scroll Rect和Mask这两个组件,修改Scroll Rect的Content参数和Vertical Scrollbar参数,并把上一步骤里的两个组件绑定上去。其中,Scroll Rect中有几个常用参数需要解释一下,可以根据自己的需求修改:
- Movement Type指的是滑块拖动到边界时的情况(Unrestricted使得滑块能无限拖拽纵然没有内容可以显示,Elastic允许超过边界但会触发弹回动画,Clamped不允许拖动到没有内容的地方);
- Viewpoint 用来指定滚动窗口大小;
- Scroll Sensitivity指的是玩家使用鼠标滚轮拖动时的灵敏度;
- Horinzontal Scrollbar和Vertical Scrollbar分别用于绑定水平滚动条和垂直滚动条;
- Visbility指定是内容不足时,Scrollbar是否显示(Permanent表示无论是否有内容都会显示滚动条,Auto Hide表示内容不足时隐藏滚动条);
6. UI效果
至此,控制台界面的设计已经基本完成了,之后就是根据自己的审美,调节字体大小、颜色等参数了。最终的控制台面板应该如下图所示:
二、控制台功能实现
考虑到控制台指令的功能与控制台界面的刷新显示是两个相互独立的模块,那么我们也要相应的设计两个类:控制UI刷新的脚本类,通过指令执行方法的功能类。这两个类之间存在一个双向交互,即用户输入的字符串通过UI类传递给功能类,回调数据(报错或警告信息)从功能类传递给UI类。
1. UI控制脚本
对于UI而言,我们只需要执行4种基本功能:输出回调信息,清屏,按下“上”键获取前向指令,按下“下”键获取后向指令。这些我们都只要通过判断用户的按键事件就可以完成了,复杂的方法则交给功能实现类来完成。ConsoleController类的完整代码如下:
using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// 控制台控制器脚本类。
/// </summary>
public class ConsoleController : MonoBehaviour
{
#region 可视变量
[SerializeField] [Tooltip("控制台输入框对象。")] private InputField inputField = null;
[SerializeField] [Tooltip("控制台输出框对象。")] private Text ouputText = null;
#endregion
#region 功能方法
/// <summary>
/// 第一帧调用之前触发。
/// </summary>
private void Start()
{
inputField.ActivateInputField();
}
/// <summary>
/// 在帧刷新时触发。
/// </summary>
private void Update()
{
string input = inputField.text; // 获取输入文本
// 按下回车输入指令
if (Input.GetKeyDown(KeyCode.Return))
{
if (!input.Equals(""))
{
ouputText.text += ">>" + input + "\n";
string output = Console.Input(input);
if (output != null)
{
// 回调信息为cls时清空控制台面板内容
if (output.Equals("cls"))
ouputText.text = "";
else
ouputText.text += output + "\n";
}
inputField.text = "";
}
}
// 按下上跳转到上一条指令
else if (Input.GetKeyDown(KeyCode.UpArrow))
inputField.text = Console.Last();
// 按下下跳转到下一条指令
else if (Input.GetKeyDown(KeyCode.DownArrow))
inputField.text = Console.Next();
inputField.ActivateInputField();
}
#endregion
}
2. 功能实现类
在UI类当中,我们的部分功能直接调用了几个方法来执行,这部分方法就是功能实现类里需要进行封装的。
- 回调信息输出。封装一个Input()方法,通过switch来选择执行的内容:
/// <summary>
/// 向控制台输入指令。
/// </summary>
/// <param name="input">指令字符串。</param>
/// <returns>回调信息。</returns>
public static string Input(string input)
{
// 分割字符串获取参数列表
List<string> args = new List<string>(input.Split(' '));
consoleHistory.Add(input);
position = consoleHistory.Count;
// 控制与回调
string output = null;
switch (args[0])
{
// 各种指令内容
default:
output = "No such command.";
break;
}
return output;
}
- 历史指令选择。定义一个字符串列表用来记录历史值,定义一个int型作为字符串列表的指针,用来记录当前指向的指令内容。封装一个Next()和Last()方法改变指针位置:
/// <summary>
/// 获取控制台上一条历史记录。
/// </summary>
/// <returns>上一条指令字段。</returns>
public static string Last()
{
if (position == -1)
return null;
position -= 1;
if (position < 0)
position = 0;
return consoleHistory[position];
}
/// <summary>
/// 获取控制台下一条历史记录。
/// </summary>
/// <returns>下一条指令字段。</returns>
public static string Next()
{
if (position == -1)
return null;
position += 1;
if (position >= consoleHistory.Count)
position = consoleHistory.Count - 1;
return consoleHistory[position];
}
综上所述,Console类内完整的代码如下:
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 控制台静态类,用于程序内部调试。
/// </summary>
public static class Console
{
# region 指令列表
private static readonly string[] command =
{
"help",
"cls",
"test"
};
# endregion
# region 静态成员变量
private static int position = -1; // 当前读取历史记录的位置
private static List<string> consoleHistory = new List<string>(); // 控制台历史记录
# endregion
# region 静态公有方法
/// <summary>
/// 向控制台输入指令。
/// </summary>
/// <param name="input">指令字符串。</param>
/// <returns>回调信息。</returns>
public static string Input(string input)
{
// 分割字符串获取参数列表
List<string> args = new List<string>(input.Split(' '));
consoleHistory.Add(input);
position = consoleHistory.Count;
// 控制与回调
string output = null;
switch (args[0])
{
// 帮助
case "help":
output = Show();
break;
// 清空控制台
case "cls":
output = Clear();
break;
// 测试
case "test":
output = Test();
break;
// 错误指令
default:
output = "No such command.";
break;
}
return output;
}
/// <summary>
/// 获取控制台上一条历史记录。
/// </summary>
/// <returns>上一条指令字段。</returns>
public static string Last()
{
if (position == -1)
return null;
position -= 1;
if (position < 0)
position = 0;
return consoleHistory[position];
}
/// <summary>
/// 获取控制台下一条历史记录。
/// </summary>
/// <returns>下一条指令字段。</returns>
public static string Next()
{
if (position == -1)
return null;
position += 1;
if (position >= consoleHistory.Count)
position = consoleHistory.Count - 1;
return consoleHistory[position];
}
#endregion
#region 静态私有方法
/// <summary>
/// 显示全部控制台命令。
/// </summary>
/// <returns>回调信息。</returns>
private static string Show()
{
string output = null;
for (int i = 0; i < command.Length; i++)
{
output += command[i];
if (i != command.Length - 1)
output += "\n";
}
return output;
}
/// <summary>
/// 清空控制台记录。
/// </summary>
/// <returns>回调信息。</returns>
private static string Clear()
{
position = -1;
consoleHistory.Clear();
return "cls";
}
#endregion
#region 控制台方法
/// <summary>
/// 测试方法。
/// </summary>
/// <returns>回调信息。</returns>
private static string Test()
{
GameObject gameObject = Resources.Load("Test") as GameObject;
if (gameObject)
{
GameObject.Instantiate(gameObject);
return "Object has been generated.";
}
return "There have no such object.";
}
#endregion
}
3. 绑定UI控件
在Console Panel中添加组件Console Controller,并绑定输入输出对象:
三、控制台的显示和隐藏
前面两节讲述的是控制台内部的设计,这一节要讲的是,控制控制台的显示与隐藏。为了使类之间低耦合,方便复用与移植,我们设计两个小类:FloatPanel类作为组件绑定在Console Panel对象上,用于控制当前对象的隐藏与显示,GameController类作为组件绑定的全局的事件对象上,用于绑定用户的键盘输入事件。这两个类的完整代码分别如下:
using UnityEngine;
/// <summary>
/// 悬浮面板组件类。
/// </summary>
public class FloatPanel : MonoBehaviour
{
# region 功能方法
/// <summary>
/// 第一帧调用之前触发。
/// </summary>
private void Start()
{
gameObject.SetActive(false); // 初始化为隐藏
}
/// <summary>
/// 显示当前对象。
/// </summary>
public void Show()
{
gameObject.SetActive(true);
}
/// <summary>
/// 隐藏当前对象。
/// </summary>
public void Hide()
{
gameObject.SetActive(false);
}
# endregion
}
using UnityEngine;
public class GameController : MonoBehaviour
{
#region 可视变量
[SerializeField] [Tooltip("控制台对象。")] private ConsoleController consoleController = null;
#endregion
#region 基础私有方法
/// <summary>
/// 在帧刷新时触发。
/// </summary>
private void Update()
{
// 按下打开控制台
if (Input.GetKeyUp(KeyCode.Tab))
consoleController.gameObject.GetComponent<FloatPanel>().Show();
// 按下退出控制台
else if (Input.GetKeyUp(KeyCode.Escape))
consoleController.gameObject.GetComponent<FloatPanel>().Hide();
}
#endregion
}
将FloatPanel组件添加在Console Panel上,将GameController添加在EventSystem上,并分别绑定对象:
测试
按下Table键,打开控制台,输入“test”指令后,就会输出一段回调信息,并在世界坐标系中创建一个新的Prefab对象实现对游戏内容的修改。出现如下图的效果,就表示成功了,之后只需要在Console类中添加新的对应指令就可以完成自定义的控制台功能了。