Design Hub

    • 注册
    • 登录
    • 搜索
    • 版块
    • 最新

    一个开发杂记贴

    游戏开发
    1
    18
    1552
    正在加载更多帖子
    • 从旧到新
    • 从新到旧
    • 最多赞同
    回复
    • 在新帖中回复
    登录后回复
    此主题已被删除。只有拥有主题管理权限的用户可以查看。
    • Pamisu
      Pamisu 管理员 最后由 编辑

      总之是游戏开发相关的比较琐碎的一些记录,包括但不限于代码片段、功能实现思路、疑难杂症、ChatGPT问答记录(可能含有欺骗性内容)、学习笔记、运行效果截图、养生指南等等,不断更新中。

      1 条回复 最后回复 回复 引用
      • Pamisu
        Pamisu 管理员 最后由 编辑

        UE中如何以固定角度变化量从四元数A平滑变换到四元数B?

        1. 速度乘以时间得出角度变化量
        2. 两四元数夹角,即总的角度
        3. 变化量除以总量,即为Slerp所需的Alpha

        a7815c11-7c84-4ed5-b4dc-e58df55c27ff-image.png

        Unity中类似的API

        1 条回复 最后回复 回复 引用
        • Pamisu
          Pamisu 管理员 最后由 编辑

          C#中的值类型与引用类型,堆内存与栈内存

          值类型:数字、布尔、复合(enum、struct)
          引用类型:数组、字符串、类、接口、委托
          值类型在栈中分配、读取速度快、内存自动回收、参数传递时开辟新空间;引用类型在堆中分配、读取速度慢、内存由GC回收、参数传递时传引用

          a8ceacc1-5e09-448c-9a4c-9faff22b8557-image.png

          栈是由高地址向低地址增长。堆是由低地址向高地址增长。
          当栈或堆现有的大小不够用的时候,它将按照图中的增长方向扩大自身的尺寸,直到预留的空间被用完为止。

          1 条回复 最后回复 回复 引用
          • Pamisu
            Pamisu 管理员 最后由 编辑

            Don'tDestroyOnLoad 对象管理

            c0b75675-6a32-4ba2-bdc2-ee2cc2046207-image.png

            709f9350-c259-4c16-bc7f-bb349706b2fb-image.png

            1 条回复 最后回复 回复 引用
            • Pamisu
              Pamisu 管理员 最后由 编辑

              Task.Factory.StartNew 和 Task.Run 到底有什么区别?

              https://blog.csdn.net/sD7O95O/article/details/124137932
              简而言之,使用 Task.Factory.StartNew 必须等待 AttachedToParent 任务执行完,而 Task.Run 不必。

              但在Unity客户端中,我推荐使用宇宙第一NB的UniTask,0GC、与引擎更加契合,更适合使用Unity的程序宝宝

              UniTask使用文档

              1 条回复 最后回复 回复 引用
              • Pamisu
                Pamisu 管理员 最后由 Pamisu 编辑

                URP在iPhone SE与Switch下的渲染设置示例

                作为低端机型的参考配置比较有用。

                fcc28111-b35e-4403-8d2e-310060508210-image.png

                4af47e60-f707-43ba-bcf7-cc9c50b9c2a5-image.png

                1d4da448-092e-442c-86b4-e35ef4a17ab1-image.png

                66677a7a-e624-4f9f-81a6-544e6c5114d6-image.png

                0a2df3ef-5de6-427d-8669-c889a0fa5a71-image.png

                db4a8703-0bf2-4493-a829-6277f6357fe2-image.png

                1 条回复 最后回复 回复 引用
                • Pamisu
                  Pamisu 管理员 最后由 编辑

                  什么是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
                  
                  1 条回复 最后回复 回复 引用
                  • Pamisu
                    Pamisu 管理员 最后由 Pamisu 编辑

                    Godot 3.5 与 Unity 2021.3.12f1 LTS 打包对比

                    02f1ee6f-cbd7-4746-8be2-d86428260803-image.png

                    c98f1448-7547-4ea8-9369-c91b00b4cb5c-image.png

                    Pamisu 1 条回复 最后回复 回复 引用
                    • Pamisu
                      Pamisu 管理员 最后由 Pamisu 编辑

                      Unity 新InputSystem如何实现键位配置功能

                      How To Implement Key Rebinding | Unity Input System Tutorial
                      https://www.youtube.com/watch?v=dUCcZrPhwSo

                      406302d9-a530-4930-9b55-fb52cac9aa5e-image.png

                      d542eee7-1fda-4f16-9415-1503288f0675-image.png

                      那么如何保存键位配置呢

                      How To Create Persistent Key Bindings | Unity Input System Tutorial
                      https://www.youtube.com/watch?v=DBBbpbIRoIc

                      Input System自带将按键配置转为Json的方法,可以将json存起来

                      d6619f79-1e6e-4ddd-8f19-294b295d6c4e-image.png

                      1 条回复 最后回复 回复 引用
                      • Pamisu
                        Pamisu 管理员 最后由 Pamisu 编辑

                        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()

                        1 条回复 最后回复 回复 引用
                        • Pamisu
                          Pamisu 管理员 最后由 编辑

                          怎么在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 }

                          1 条回复 最后回复 回复 引用
                          • Pamisu
                            Pamisu 管理员 最后由 编辑

                            个人版跳过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一闪而过的情况。通常来说,游戏的首场景也不应该放太多东西,这样可以尽可能地精简首包大小,提高启动速度。

                            1 条回复 最后回复 回复 引用
                            • Pamisu
                              Pamisu 管理员 @Pamisu 最后由 编辑

                              Godot 4.1 对比

                              5bc7fa93-efac-41e6-a3a9-ce49ede8ab7a-image.png

                              1 条回复 最后回复 回复 引用
                              • Pamisu
                                Pamisu 管理员 最后由 Pamisu 编辑

                                Godot Unity 控制按钮点击区域(奇形怪状的按钮)

                                Godot中,TextureButton中有一个Click Mask属性,可以设置一张位图来控制可点击区域,白色部分会触发点击,黑色部分不触发:
                                57304114-ffe1-4761-9ba1-8d0324116a99-6eea0d12b8a9781482433737d3109249.jpeg

                                在Unity中同样有办法实现,首先按钮图片的导入设置中需要开启Read/Write:
                                56b5beb5-a9c3-404d-a780-6d80e07c5423-image.png

                                然后在脚本中获取到按钮上的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/

                                1 条回复 最后回复 回复 引用
                                • Pamisu
                                  Pamisu 管理员 最后由 Pamisu 编辑

                                  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在之前已经被预加载了,这里应该会立即返回结果,然而实际测试发现并没有在同一帧内执行:

                                  61a69c65-52d2-49cf-8ddd-a2bc72b84986-image.png

                                  通过Event Viewer查看,也发现SomePrefab在预加载时被加载了一次,使用时又被加载了一次,等于没预加载。

                                  到底怎么烩柿呢,尝试修改预加载部分:

                                  var labels = new List<string> { "Preload", "SomeScene" };
                                  await Addressables.LoadAssetsAsync<GameObject>(labels, null, Addressables.MergeMode.Intersection).ToUniTask();
                                  

                                  只是将Object修改为了GameObject,再次运行发现预加载生效了,使用时立即返回了已经加载好的SomePrefab:

                                  c6a0b8d9-4e71-4698-8c20-536e98a8f94d-image.png

                                  具体原因还没有深入研究,总之先记录一下。

                                  1 条回复 最后回复 回复 引用
                                  • Pamisu
                                    Pamisu 管理员 最后由 Pamisu 编辑

                                    反思一下在自己项目和别人项目中遇到的国际化/本地化问题

                                    • 程序直接在代码里写死文本/资源路径
                                    • 翻译后的文本一般在接近项目完成时才能给到,在这之前要有办法提前测试,Godot有一个“伪本地化”模式,可以把文本临时翻译成火星文,没变成火星文的地方说明有遗漏
                                    • 文本大小/长度适应,不同语言、字体下的文本长度和大小会不一样,设计和制作UI时需要提前考虑到
                                    • 字体替换,静态本地化通常不会同时包含多个字体,针对不同语言的版本需要能切换字体,同时需要注意切换字体后是否能正常显示,没做好文本大小适应的情况下很可能换字体后一大片都不显示
                                    • 动态本地化要提前封装好相关组件(文本、图片等),并且严格禁止对原生组件直接设值的行为(不然就做不到动态了)
                                    • 国际化/本地化表格多人协作问题,暂时没什么头猪,倒是见过用csv的
                                    1 条回复 最后回复 回复 引用
                                    • Pamisu
                                      Pamisu 管理员 最后由 Pamisu 编辑

                                      Unity Animator在物体被禁用时保留状态与参数

                                      默认情况下,物体被禁用时Animator会回到初始状态并且重置所有参数,要保留状态和参数,使用:

                                      animator.keepAnimatorStateOnDisable = true;
                                      

                                      需要Unity 2018.1以上版本。

                                      1 条回复 最后回复 回复 引用
                                      • Pamisu
                                        Pamisu 管理员 最后由 编辑

                                        Unity 打包后TileMap的碰撞体无法动态更新

                                        游戏中有修改地形功能时(例如创造或炸毁地块)需要在运行时更新TileMap,在编辑器中碰撞体可以随之更新,而打包后却无法更新,这种情况需要在对应Sprite导入设置中勾选Read/Write Enabled选项,如果是图集,则勾选图集的该选项:
                                        813d9b41-72f8-4738-ba16-0e4bf7991391-image.png

                                        1 条回复 最后回复 回复 引用
                                        • 1 / 1
                                        • First post
                                          Last post
                                        版权所有 ©2023 Design Hub 保留所有权利
                                        鄂ICP备2021004164号-2