一个开发杂记贴
-
总之是游戏开发相关的比较琐碎的一些记录,包括但不限于代码片段、功能实现思路、疑难杂症、ChatGPT问答记录(可能含有欺骗性内容)、学习笔记、运行效果截图、
养生指南等等,不断更新中。 -
-
C#中的值类型与引用类型,堆内存与栈内存
值类型:数字、布尔、复合(enum、struct)
引用类型:数组、字符串、类、接口、委托
值类型在栈中分配、读取速度快、内存自动回收、参数传递时开辟新空间;引用类型在堆中分配、读取速度慢、内存由GC回收、参数传递时传引用栈是由高地址向低地址增长。堆是由低地址向高地址增长。
当栈或堆现有的大小不够用的时候,它将按照图中的增长方向扩大自身的尺寸,直到预留的空间被用完为止。 -
Don'tDestroyOnLoad 对象管理
-
Task.Factory.StartNew 和 Task.Run 到底有什么区别?
https://blog.csdn.net/sD7O95O/article/details/124137932
简而言之,使用 Task.Factory.StartNew 必须等待 AttachedToParent 任务执行完,而 Task.Run 不必。但在Unity客户端中,我推荐使用宇宙第一NB的UniTask,0GC、与引擎更加契合,更适合使用Unity的程序宝宝
-
URP在iPhone SE与Switch下的渲染设置示例
作为低端机型的参考配置比较有用。
-
什么是CBUFFER,为什么要用它
Unity不为我们提供模型-视角-投影矩阵,因为M和VP矩阵的相乘是可以避免的。除此之外,对于被同一相机中渲染的物体,VP矩阵在每帧可以被重复使用(将坐标从世界空间变换到投影空间)。Unity的shader重复利用这一事实,将这些矩阵放在不同的常量缓存区中。虽然我们将它定义为变量,但它们的数据在绘制单个图形的时间内保持不变,甚至往往保持的更久。VP矩阵可以放在逐帧(per-frame)缓存区,而M矩阵则保存在逐绘制(per-draw)缓存区。
虽然没有强制要求将这些shader变量放在常量缓存区,但是这么做可以更有效地更改同一缓冲区中的所有数据。至少在对应的图形api支持的时候是这样的。不过OpenGL不支持。
为了更有效率,我们充分利用常量缓存区。Unity将VP矩阵放在UnityPerFrame缓存区,把M矩阵放在UnityPerDraw缓存区。有很多数据都可以放在这些缓存区中。一个常量缓存区像结构体一样定义,但是使用
cbuffer
关键字。cbuffer UnityPerFrame { float4x4 unity_MatrixVP; }; cbuffer UnityPerDraw { float4x4 unity_ObjectToWorld; };
因为常量缓存区并不对所有平台有增益,所以Unity的shader依赖宏来确保只在需要时使用它们。用宏
CBUFFER_START
带一个名字参数来代替cbuffer
,以宏CBUFFER_END
来作为缓冲区的结尾。
这个宏是在核心库中定义的,所以需要先引入核心库。#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl" CBUFFER_START(UnityPerFrame) float4x4 unity_MatrixVP; CBUFFER_END CBUFFER_START(UnityPerDraw) float4x4 unity_ObjectToWorld; CBUFFER_END
那么这一段很好理解了,并不是所有的值都需要在每个物体每次绘制时由CPU提交给GPU,有些数据是可以在一定的时间段共用的,CBUFFER的目的便是将这些值放入常量缓存区以提升性能。而UnityPerFrame UnityPerDraw UnityPerMaterial则代表着不同类型的常量缓存区。
CBUFFER_START(UnityPerMaterial) float4 _MainTex_ST; half4 _BaseColor; CBUFFER_END
-
Godot 3.5 与 Unity 2021.3.12f1 LTS 打包对比
-
Unity 新InputSystem如何实现键位配置功能
How To Implement Key Rebinding | Unity Input System Tutorial
https://www.youtube.com/watch?v=dUCcZrPhwSo那么如何保存键位配置呢
How To Create Persistent Key Bindings | Unity Input System Tutorial
https://www.youtube.com/watch?v=DBBbpbIRoIcInput System自带将按键配置转为Json的方法,可以将json存起来
-
Unity 为什么应该使用obj.CompareTag("Player")代替obj.tag == "Player"
在使用后者时,Rider会有个提示Explicit string comparison is inefficient, use 'CompareTag' instead,前者是Unity内置的函数,可以避免额外的内存分配。
那么后者哪里出现了额外的内存分配呢,答案是obj.tag,get方法返回了tag的复制:public string tag { get => this.gameObject.tag; set => this.gameObject.tag = value; }
这个tag的复制需要被垃圾回收器回收。
光看上面的代码不太能看出哪里返回复制了,在C#中只要不改变字符串,就不会产生额外的内存分配,查了一下有个说法是旧版Unity中这里会有额外的内存分配,新版已经和CompareTag性能相当,但处于安全性和可读性考虑依然推荐使用CompareTag。
类似地,使用Input.GetTouch()和Input.touchCount代替Input.touches,使用Physics.SphereCastNonAlloc() 代替 Physics.SphereCastAll()
-
怎么在Luban中分割一个map
文档仅介绍了列表与bean的分割,例如:
假装这是一张Excel表...
##var name_list ##type (list#sep=,),string 王诛魔,李杀神,刘斩仙 张三,李四
会生成 [ "王诛魔", "李杀神", "刘斩仙" ] 等等。
sep会拆分单元格和字符串,再流式入,sep可以包含多个字符,如 sep=",;",此时会用每个字符来拆分读入的字符串。
所以对于map可使用:
##var name_power ##type (map#sep=,;),string,float 王诛魔,1000;李杀神,1200;刘斩仙,900 张三,100;李四,5
会生成 { "王诛魔":1000, "李杀神":1200, "刘斩仙":900 }
-
个人版跳过Unity闪屏Logo
创建一个脚本,粘贴以下代码即可:
namespace Game { #if !UNITY_EDITOR using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Scripting; [Preserve] public class SkipSplashScreen { [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSplashScreen)] private static void BeforeSplashScreen() { #if UNITY_WEBGL Application.focusChanged += Application_focusChanged; #else System.Threading.Tasks.Task.Run(AsyncSkip); #endif } #if UNITY_WEBGL private static void Application_focusChanged(bool obj) { Application.focusChanged -= Application_focusChanged; SplashScreen.Stop(SplashScreen.StopBehavior.StopImmediate); } #else private static void AsyncSkip() { SplashScreen.Stop(SplashScreen.StopBehavior.StopImmediate); } #endif } #endif }
首场景越小,效果越好,首场景较大的情况下,可能会出现Logo一闪而过的情况。通常来说,游戏的首场景也不应该放太多东西,这样可以尽可能地精简首包大小,提高启动速度。
-
Godot 4.1 对比
-
Godot Unity 控制按钮点击区域(奇形怪状的按钮)
Godot中,
TextureButton
中有一个Click Mask
属性,可以设置一张位图来控制可点击区域,白色部分会触发点击,黑色部分不触发:
在Unity中同样有办法实现,首先按钮图片的导入设置中需要开启
Read/Write
:
然后在脚本中获取到按钮上的Image组件,修改其
alphaHitTestMinimumThreshold
属性:private Image _clickMaskImage; [SerializeField] private float _clickMaskAlphaThreshold = .1f; private void Awake() { _clickMaskImage = GetComponent<Image>(); _clickMaskImage.alphaHitTestMinimumThreshold = _clickMaskAlphaThreshold; }
这个属性的默认值为0,图片所在的方形区域都会拦截点击事件,当值大于0时,图片中透明度大于该值的地方才可以被点击。
论坛里关于这个问题的讨论,2020年之前的解决方案都相对比较复杂:
https://forum.unity.com/threads/none-rectangle-shaped-button.263684/ -
Unity Addressables踩坑
某些情况下,需要在走进度条时提前加载接下来要用到的资源,以免用时再加载导致长时间卡顿,一个写法是:
因为要预加载的资源可能会有很多,所以通过标签来加载,将所有同时标有“Preload”和“Combat”标签的资源加载到内存:
// 预加载部分 var labels = new List<string> { "Preload", "Combat" }; await Addressables.LoadAssetsAsync<Object>(labels, null, Addressables.MergeMode.Intersection).ToUniTask();
因为要预加载的资源类型不只有GameObject,还有Sprite、Material等等,这里指定类型为Object,这样可以加载所有类型的资源。
然后到具体的使用处,使用地址来加载需要的资源:
Debug.Log(Time.frameCount); // 测试打印当前帧1 // 通过地址加载SomePrefab var prefab = await Addressables.LoadAssetAsync<GameObject>("SomePrefab").ToUniTask(); Debug.Log(Time.frameCount); // 测试打印当前帧2
按理来说,SomePrefab在之前已经被预加载了,这里应该会立即返回结果,然而实际测试发现并没有在同一帧内执行:
通过Event Viewer查看,也发现SomePrefab在预加载时被加载了一次,使用时又被加载了一次,等于没预加载。
到底怎么烩柿呢,尝试修改预加载部分:
var labels = new List<string> { "Preload", "SomeScene" }; await Addressables.LoadAssetsAsync<GameObject>(labels, null, Addressables.MergeMode.Intersection).ToUniTask();
只是将Object修改为了GameObject,再次运行发现预加载生效了,使用时立即返回了已经加载好的SomePrefab:
具体原因还没有深入研究,总之先记录一下。
-
反思一下在自己项目和别人项目中遇到的国际化/本地化问题
- 程序直接在代码里写死文本/资源路径
- 翻译后的文本一般在接近项目完成时才能给到,在这之前要有办法提前测试,Godot有一个“伪本地化”模式,可以把文本临时翻译成火星文,没变成火星文的地方说明有遗漏
- 文本大小/长度适应,不同语言、字体下的文本长度和大小会不一样,设计和制作UI时需要提前考虑到
- 字体替换,静态本地化通常不会同时包含多个字体,针对不同语言的版本需要能切换字体,同时需要注意切换字体后是否能正常显示,没做好文本大小适应的情况下很可能换字体后一大片都不显示
- 动态本地化要提前封装好相关组件(文本、图片等),并且严格禁止对原生组件直接设值的行为(不然就做不到动态了)
- 国际化/本地化表格多人协作问题,暂时没什么头猪,倒是见过用csv的
-
Unity Animator在物体被禁用时保留状态与参数
默认情况下,物体被禁用时Animator会回到初始状态并且重置所有参数,要保留状态和参数,使用:
animator.keepAnimatorStateOnDisable = true;
需要Unity 2018.1以上版本。
-
Unity 打包后TileMap的碰撞体无法动态更新
游戏中有修改地形功能时(例如创造或炸毁地块)需要在运行时更新TileMap,在编辑器中碰撞体可以随之更新,而打包后却无法更新,这种情况需要在对应Sprite导入设置中勾选Read/Write Enabled选项,如果是图集,则勾选图集的该选项: