"程序员的一半生命都浪费在了调试上。"
——Brian Kernighan(计算机科学家,曾参与开发C语言)
(图片来源:forbesindia)
Debug无疑是程序员最头疼,也是耗费时间最多的一个环节,因此如何提高Debug效率至关重要。本文将会根据小棋自身多年的开发经验,总结和分享五种常用的调试方式,帮助你提高开发效率。
注:案例中会使用到C#和python语言,其他语言基本同理。
一、Log
> 看起来平平无奇,却是最基础最简洁的debug方式,在没有Vscode等编辑器之前,是最常用的调试方式啦。O(∩_∩)O~
1. python
>>> print("hello world")
hello world
2. C#
// C#
Console.WriteLine("hello world")
// Unity对此进行了封装
Debug.Log("hello world");
print("hello world");
其实Unity中print底层调用的也是Debug.Log
3. Log如何帮助我们得到有用信息?
学会如何打Log也是一个重要编程能力,举个简单的例子:Unity开发游戏过程中经常会提示空指针异常。
NullReferenceException: Object reference not set to an instance of an object
这时候我们可以把Log打在报错附近,查看报错附近的代码语义:
ResManager.LoadPrefab(path);
可以看到这里尝试加载某个路径的物体,尝试打印一些关键信息,比如:
print(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
print(path);
其中,第一行是一个显眼的标识,方便快速定位log,第二行是关键信息。最终结果:
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
Panel/MenuPanel1
然后到加载路径一看,原来路径下只有Panel/MenuPanel,没有Panel/MenuPanel1,修改为正确路径,这样问题就得到解决了。
二、traceback
traceback是我入职当年,我认为导师教我的最简单又好用的功能。有时候游戏在运行过程中,莫名其妙执行了一个操作:比如玩家在某个时间执行了两次攻击逻辑,又或者在错误的时间某个UI界面弹出了。
总结下,适合traceback调试有两个基本要求:
-
攻击和UI打开的执行逻辑你知道在哪里 (*^▽^*)
-
为什么会执行,谁执行了这个逻辑,不知道 o(╥﹏╥)o
这时候怎么做呢?
1、Python
在python中非常简洁:
在被调用的方法中执行,traceback.print_stack。
当这个方法被执行的时候,我们就清楚的知道是谁执行了这个代码。
2. Unity
Unity游戏引擎深知traceback对于开发效率的提升,因此在编辑器中我们甚至无需做任何处理,就能得到调用栈的信息。
下方有完整的traceback调用栈,双击log信息还能跳转到指定的代码行数,非常方便。
但是这有个前提,需要再Project Settings -> Player -> Other 对Stack Trace进行设置,将其统一设置为ScriptOnly就ok了。(这个设置是默认的)
三、把Log输出到文件中
当你把游戏打开输出之后,或者把python编译成exe运行时,没有了编辑器,也没有log窗口了。这时候我们该如何调试项目呢?
把Log输出到文本文件中就是一个不错的方法,市面上主流的软件也都会执行这一操作。
这里有两种办法供大家选择:
1. 方法一,写一个新的Log.Print方法,调用这个方法的log会输出到文件中。
2. 方法二,用户可以继续使用系统内置的打印方法,不改变用户习惯,我们只监听或者劫持系统的打印方法,并将其输出到log文件中。
显然,第二种方法是更好的。其中Unity用于监听默认打印事件的方法是:
Application.logMessageReceived
而python对应的事件是:
sys.stdout
其他语言也有类似的输出流。
这里我以Unity为例,提供一个参考案例(抄了代码记得点点在看和收藏呀,求求您啦~):
using UnityEngine;
using System.IO;
using System;
using System.Diagnostics;
public class LogToFile : MonoBehaviour
{
private string logFilePath;
private StreamWriter streamWriter;
void Start()
{
// 创建一个新的文件名,包含时间戳
string fileName = "log_" + DateTime.Now.ToString("yyyyMMdd_HHmmss") + ".txt";
// 获取日志文件夹的路径
string logFolder = Application.persistentDataPath + "/log";
// 确保日志文件夹存在
Directory.CreateDirectory(logFolder);
// 拼接出完整的日志文件路径
logFilePath = Path.Combine(logFolder, fileName);
// 开始写入日志
streamWriter = File.AppendText(logFilePath);
// 监听日志事件
Application.logMessageReceived += LogMessageHandler;
}
void OnDestroy()
{
// 关闭文件流
if (streamWriter != null)
{
streamWriter.Close();
}
}
void LogMessageHandler(string logString, string stackTrace, LogType type)
{
// 获取当前时间
string currentTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
// 使用 System.Diagnostics.StackTrace 获取调用栈信息
StackTrace trace = new StackTrace(true);
// 获取最后一个帧的栈帧
StackFrame frame = trace.GetFrame(trace.FrameCount - 1);
// 获取调用的脚本路径
string scriptPath = Path.GetFileName(frame.GetFileName());
// 获取调用的行号
int lineNumber = frame.GetFileLineNumber();
// 将日志信息写入文件
streamWriter.WriteLine(currentTime + " [" + type + "] " + scriptPath + "(" + lineNumber + "): " + logString);
// 强制刷新缓冲区,确保日志信息立即写入文件
streamWriter.Flush();
}
}
其中,关键代码是:
// 监听日志事件
Application.logMessageReceived += LogMessageHandler;
其他代码主要是对log信息进行整理,提供:
-
事件信息
-
调用栈信息
-
调用行信息
把LogToFile文件放到场景中的某个物体上后,来看看执行后的效果:
2024-03-31 16:55:37 [Log] LoginScene.cs(11): >>>>>>>>>>>>>>>>>>>>>> openUI: MenuPanel
2024-03-31 16:55:37 [Exception] (0): ArgumentException: The Object you want to instantiate is null.
2024-03-31 16:55:37 [Log] MenuPanel.cs(55): >>>> start
2024-03-31 16:55:37 [Exception] (0): NullReferenceException: Object reference not set to an instance of an object
非常方便的可以定位到问题。
四、点击调试
点击调试主要讲游戏开发,这里以Unity举例。
对于用户输入来说,鼠标和手指点击是非常重要的一个操作,但是开发过程中经常发现点不到我们想要的物体,这时候可以通过射线检测判断下当前点击事件被哪些物体拦截了。
下面提供代码:
using UnityEngine;
using UnityEngine.EventSystems;
public class ClickDetector : MonoBehaviour
{
void Update()
{
// 检测鼠标左键点击
if (Input.GetMouseButtonDown(0))
{
// 检查是否点击在 UI 上
if (EventSystem.current.IsPointerOverGameObject())
{
// 获取点击的 UI 元素
GameObject clickedObject = EventSystem.current.currentSelectedGameObject;
// 打印 UI 元素的名称
if (clickedObject != null)
{
Debug.Log("Clicked on UI element: " + clickedObject.name);
}
else
{
Debug.Log("Clicked on UI element, but no object found.");
}
}
else
{
// 如果没有点击在 UI 上,则检查物体
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit))
{
// 打印物体的名称
Debug.Log("Clicked on object: " + hit.collider.gameObject.name);
}
else
{
Debug.Log("Clicked, but no object found.");
}
}
}
}
}
具体使用方法,就是把这个脚本放到场景中,然后鼠标点到某个物体,就会打印出指定的物体名称。如果是因为某个物体挡在前面导致被拦截,就能很快的找到问题所在了。
五、断点调试
断点调试是一种在程序执行过程中暂停执行并允许程序员检查程序状态的调试技术。
在大多数集成开发环境(IDE)和调试器中,你可以通过点击编辑器的某一行代码旁边的区域(通常是左侧)来设置断点
当程序执行到这一行时,会自动停止执行,然后你可以逐步执行代码、观察变量值、检查堆栈跟踪等。
因为之前在其他平台已经发过了,所以这里就简单贴下链接,感兴趣的同学可以跳转过去看看。
)
欢迎大家后台私信补充更多效率开发的方法,目前公众号还不支持留言功能,会尽快想办法哒~
想了解更多游戏开发知识,可以扫描下方二维码,免费领取游戏开发4天训练营课程
好啦,以上就是本期分享的内容。