Unity 内存管理的专业提示

优化是游戏开发中很重要的一部分,涉及的方面主要有 CPU,GPU,内存,网络等。 在这篇博客中,主要会写关于内存相关的优化,Unity 是如何管理内存的,以及怎样才能使游戏更高效地运行。 在深入到 Unity 之前,我们先来看一下内存是如何工作的。 操作系统会在物理内存的基础上,创建一个虚拟内存,用于程序运行。在这个抽象中,会有一些虚拟地址空间(VAS) 在内存优化上,主要考虑两部分,也就是栈(Stack)和堆(Heap)。在理解栈和堆之前,首先要理解在 Unity 中,数据的类型,可分为值类型,和引用类型。对于值类型的数值,会直接存储在栈上,并且是可以直接复制的。例如,int, float, bool, struct, Color Vector3 等。 而引用类型的数据,则是存储在堆上,但是,指向这个数据的指针,是存储在栈上的。 栈(Stack) 存储值类型 存储引用类型的指针 由 CPU 自动管理,无须程序员关心。在函数调用时,参数将自动申请内存放入栈中,当函数结束时,这些参数又会从栈取出,释放内存,这一切操作都是自动的。 栈比堆的速度快 栈的容量有限,如果超过上限,则会出现栈溢出。 栈的运行方式是 Last in first out。也就是后进先出,或者说先进后出。 堆(Heap) 可以存储引用类型和值类型的数据 由程序员手动管理 内存空间不一定是连续的 比栈速度慢 可以扩展空间大小 接下来,我们来理解什么是 “垃圾”,以及什么是 “垃圾回收”。 垃圾 存储在栈上的一个指向堆数据的指针结束时(释放时),堆上的数据没有用了,对于这样的对象,可以理解为 “垃圾”。 垃圾回收 为了解决垃圾内存的问题,.Net 有对应的垃圾回收系统。 在初始化一个新的进程时,语言运行时会为该进程保留一个连续的地址空间区域。这个保留的地址空间称为托管堆。托管堆维持指向堆中下一个对象的地址指针。 从托管堆中分配内存,比非托管堆中分配内存要快。 垃圾回收系统会创建一个包含从根可访问的所有对相的图。当一个对象不在这个图中,也就是无法从根节点访问时,垃圾回收系统会将这部分内存视为垃圾,并在合适的时机释放为其分配的内存。 Unity 的内存管理系统 Unity 的内存管理分为三部分。 托管内存 托管内存使用托管堆以及垃圾回收系统自动分配和释放内存,而提供这个功能的是 Mono 或 IL2CPP。在 Unity 中我们大部分时候使用的都是托管内存,所以 New 对象时,都不考虑释放,垃圾回收系统会帮我们处理掉。 C# 非托管内存 C#非托管内存层使你可以在编写C#代码的方便下访问本机内存层以微调存储器分配。 您可以在Unity Core API中使用Unity.CollectionsNamespace(包括NativeArray),以及Unity Collections软件包中的数据结构来访问C#非托管的内存。如果您使用Unity的 Job System 或 Brust,则必须使用C#非托管内存。...

November 1, 2022 · 1 min · fred

异能觉醒,新游预约

4508年,因核泄露污染水域从而造成人类大灭绝,并造成了全球生物变异,但少数人类适应了污染,造成了细胞全面进化,使生命层次进化觉醒异能。 自从污染事件已来,整个世界变得异常的诡异,一些【奇奇怪怪】的东西,开始出现,什么鸡坤,什么篮球,杀伤力不强,但是让人异常头晕。在这个少数人的世界,开始一段令人哇塞的旅程。收集各种各样的物品,打造各种奇怪的装备,战斗各种让人费解的敌人。还可以养宠物,升级宠物,带着宠物去打怪…… 一旦你开始玩这个游戏,奇怪的事情,就开始发生了…… 是的,我又开发新游戏了,已经新建好文件夹,不过还没有开始写下第一行逻辑代码。 开发者的话 大家好,我是练习时长,好多年的,游戏开发者,因为技术太差,找不到工作,赚不到钱,只能白天送外卖,晚上开发游戏。我平时什么也不喜欢干,就喜欢打代码,做游戏。这个游戏,是我做的众多辣鸡,噢不,好玩的游戏之一。游戏正在开发中,预计下个月可以做完上线(如果我平时少摸点鱼的话),嗯,不管怎么样,点一下预约,点一下关注,你就是我爱的人~ 虽然游戏还没有做完,但依然可以加入QQ号瞎聊【异能觉醒 - 1群】:134032407 游戏假图 游戏预约 在 TapTap 上预约:https://www.taptap.com/app/242377 在 好游快曝 上预约:https://www.3839.com/a/147738.htm

October 17, 2022 · 1 min · fred

通过作用域最小化来避免屎一样的代码

面条式代码,是软件工程中反面模式的一种,也就是源代码的控制流程复杂,混乱难以理解,见到来说,就是屎一样的代码。 最小化变量的作用域,是避免产生面条式代码的一种有效方式。 作用域最小化是一种构建代码的方式,操作起来很容易 声明具有最小作用域的变量 用具有最小作用域的数据来分配变量 事实上,是代码的结构,定一了变量的可见性。 背景 一个程序是由一些单一或者组合的语句构成,例如赋值操作、条件判断、循环等。 通常来说,如果有两个代码块 A 和 B 如果 A 包含 B,那么 A 就是 B 的外部代码块 如果 B 被 A 所包含,那么 B 就是 A 的内部代码块 代码的缩进级别,是由嵌套级别数量决定的,直接的内部块会比其外部块多一个级别。 我们假设定义全局作用域,也就是没有外部块的代码,缩进级别为0,而全局变量,就是那些在全局范围内定义的变量。 变量可见规则 一个变量的可见性,由下面规则决定 从该变量的声明语句开始 结束于变量声明块的末尾 并且在开始与结束中间的嵌套块中也是可见的 反过来说,如果一个变量是不可见的,那么 在变量声明前 在变量声明块结束后 建议 R1. 尽量不要使用全局变量 R2. 声明单一用途的变量 R3. 在接近变量用途的地方声明变量 R4. 保持小的代码块 R5. 使用靠近其声明的变量 R6. 使用不超过2层的嵌套 R1. 尽量不要使用全局变量 全局变量会使代码变得难以阅读和理解,难以维护和测试。 全局变量的使用,增加了出现问题的概率,并且不容易被发现。 程序中可能错误地分配变量的语句越少越好。 总之,全局变量的使用往往代表着技术债务,必须尽快重构以避免后期的不可维护。 R2. 声明单一用途的变量 为单一的特定目的声明和使用变量,以便将其作用域限制作到最小。 一个变量的目的性越多,意味着可访问此变量的代码块越多。 而变量可见的语句数量越多,可能错误地赋值变量的语句就越多。 可能错误地赋值变量的语句越多,那么发现和修复潜在错误就越困难。 R3. 在接近变量用途的地方声明变量 尽可能地在靠近将使用这些变量的语句和代码块的地方声明变量。 与 R2 严格相关。...

October 4, 2022 · 1 min · fred

噪声函数与地图生成

在我学习声音信号处理的时候,我的大脑很自然地联想到了地图生成。这篇博客记录了关于信号处理的一些概念与地图生成相关的东西。这些知识点不是一些新的东西,但对我来说,是以前从未接触过的,所以我想记录一下,并且分享给大家。这篇博客会覆盖一些简单的主题,频率、振幅、噪声的种类、噪声的应用等。涉及到的数学部分,基本上只有正弦波形。 这里会从简单的概念开始,然后逐渐深入。 注意:下面涉及的代码,虽然是以 Python 来描述的(简单直观),但文章的目的是为了解释原理,使用任何语言都可以的。 1. 为什么随机性是有用的 我们在程序化的地图生成中要做的是生成一组输出,其中有一些东西是相同的,而有一些东西是不同的。例如,在我的世界这个游戏中,所有的地图都有很多相似性。生物群落,方块大小,生物群落的平均大小,洞穴的平均高度,不同石头所占的比例等等。但是,也有一些不同的地的:群落的位置,黄金的位置,洞穴的大小等等。作为游戏的设计者,需要决定哪些部分需要是相同的,哪些部分需要是不同的。 对于不同的部分,通常是使用随机数生成器。让我们来做一个极其简单的地图生成器:它将包含20个格子,其中某些格子将包含宝箱。结果如下 请注意这个地图有多少共同的地方:首先它都是由格子组成(每个点作为一个格子),每行有20个格子,然后有两种类型的块,一个是空白,一个是宝箱。 但有一点是不同的,哪一个格子是什么类型,也就是说宝箱可能出现在从0到19的任何一个格子。 我们可以使用随机数来选择将宝箱放在哪一个格子中。最简单的方式是选择从0到19的随机数。这意味着每一个格子都可能被选择。大部分的编程语言都包含随机数生成函数。在 Python 中,使用方式是 random.randint(0, 19)。完整代码如下 def gen() map = [0] * 20 pos = random.randint(0, 19) map[pos] = 1 return map for i in range(5): print_chart(i, gen()) 生成结果如下: 假设我们想让地图中的宝箱有更多的可能性出现在左边,这时就要使用非均匀随机数选择了。有很多方法可以完成这件事情,其中一种方式是首先选择一个随机数,然后将它向左移动,例如,使用函数 random(0,19)/2,下面是 Python 代码 def gen(): map = [0] * 20 pos = random.randint(0, 19) / 2 map[pos] = 1 return map for i in range(5): print_chart(i, gen()) 然后,如果我们想让宝箱更多地出现在左边,但是,右边也不能一个没有,应该怎么办呢?一个方式是使用平方数,也就是先选定一个随机数,然后计算它的平方,然后再用结果除以19(地图右边界索引),得到的结果向下取整。下面是代码和效果 def gen(): map = [0] * 20 pos = random....

September 25, 2022 · 3 min · fred

Unity 游戏启动前显示隐私协议

使用 Unity 开发的游戏,在上架某些平台时,比如 TapTap,由于政策的原因,需要在收集用户信息,或使用某些权限时,先弹出隐私协议,用户同意之后,才能操作。但是如果直接在 Unity 里做这件事情,哪怕是用一个空场景来做,Unity 本身就会在隐私协议前收集一些信息,所以,我们需要使用原生代码,来操作这一块逻辑。这里只说 Android。 首先要做的就是在 Assets/Plugins/Android 新建一个 java 文件,用来作为启动 Activity,在这个 Activity 中,先展示隐私协议,当玩家点击同意后,再去调用 Unity 的 Activity。 package com.moeif.moeifgames; import android.app.Activity; import android.app.AlertDialog; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; import com.unity3d.player.UnityPlayerActivity; public class MoeNativeActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (!isTaskRoot()) { Intent intent = getIntent(); String action = intent.getAction(); if (intent.hasCategory(Intent.CATEGORY_LAUNCHER) && action != null && action.equals(Intent.ACTION_MAIN)) { finish(); return; } } Boolean anInt = false; // 隐私协议相关 SharedPreferences base = getSharedPreferences("base",MODE_PRIVATE); anInt = base....

July 17, 2022 · 2 min · fred

一个独立游戏的开发总结

从2021年4月开始,我作为一名独立开发者,开始了自己的瞎折腾之路。一年多没有做出什么赚钱的项目。不过现在,终于做出了一个对于我自己来说,算是成功的项目。 我现在上线的这款游戏是一个文字游戏,画风稀碎,也没有什么具体的游戏类型,一切都是野路子,只要觉某个玩法好玩,就会添加这个玩法。这个游戏就是《无量:钓海》,目前已经上线了 TapTap、好游快爆、AppStore、光环助手、4399 平台。在没有版号的情况下,国内也就这些平台可以上了。 《无量钓海》TapTap 链接: https://www.taptap.com/app/234065 游戏从构思,开发,到上线,经历了很多问题,也学到很多东西。这些经验,趟过的坑,开发上的积累,等等,都可以用于下一个项目。 这个项目参与的人可以说有三个。工程的开发,是我来做,游戏内容的构思,策划,是另一个小伙伴在做。第三个人,也是一个游戏玩法,帮我们做了一部分角色的图片。 这个游戏的起点,是在我一个游戏《游戏码农》的玩家群里,有一个玩家说他有一个很好的点子,建议我做一下。正好那个时候也在想新项目的方向,我考虑了一下,所性就问他是否愿意合作开发,然后游戏的广告收益进行分成。结果已经知道了,我们合作了。而这个玩家,是一个正在读高中的学生。在合作的过程中,一开始的沟通肯定不是那么顺畅的,因为对方也没做过游戏。但最终这个问题还是解决了,方法也很简单,就是把自己构思的东西,写明白,写详细,以文字的形式描述出来。至于工具,尝试了很多,最终他用起来最顺手的还是 WPS 云文档,那就这个了,毕竟工具而已,无所谓的。作为独立开发者,我觉得不要纠结于用什么工具,不要拿公司那套完善的流程来套用,可能并不合适。一定要记住目标,就是把游戏做出来,把构思内容传达明白,至于方式,在开始的阶段,根本不重要。当然,这只是从我个人的体验角度出发,如果意见不同,那就以你为准 游戏从构思,到上线,一共是两个半月的时间,前一个月,基本上没有工程开发相关的东西,也不是没有,只是没有游戏内容上的开发,那时我记得我好像在写热更新,广告调用,等等通用的东西。游戏一开始的构思方向,和现在上线的版本,差别还是挺大的,一开始构思的内容,除了核心的钓鱼这个点,其他的都推翻了。一开始挺慌的,不确定性太大了,策划心里可能也没谱,但还是这样做了,还好,这个方向也没啥大问题。 游戏在5.16号上线了 TapTap,好游快爆。建立了玩家 QQ 群,很快突破了2000人,TapTap 上的评分,也一路上涨,当然,这一切并不是因为游戏,而是我们在 QQ 群里说让大家评论,然后有兑换码奖励。这一点一定不要学,因为 TapTap 是不允许的。因为这个操作,游戏的评分从 4 分,2天内直接冲到了9分以上,并且进了热门榜前5。这也带来了大量的玩家加入QQ 群,为此我还给腾讯贡献了好几百块,用来充年度 VIP 会员,用来扩群。 不过给兑换码让玩家评论的方式,在 TapTap 这里是不允许的。后来,TapTap 就给我们发了警告,不过一开始我没有看到,所以导致了后面大量的评论被删,而且被删的都是一些好评,差评全都留下了,这直接导致了评分的腰斩,回到了6点几分。现在还好,慢慢的恢复正常了,稳定在 7 分左右。至此,游戏的前期平台流量,也基本上宣告结束了,每日下载量巅峰时刻是 1W 多,后来逐步下降,现在完全是自然量了,TapTap 平台每日新增玩家也就几十个,加上其他的平台,现在每日新增在 150 左右。目前累计玩家总量 4.9W 左右,我自己统计的,平台统计的可能会更多一点。每日活跃玩家总数大概是 1800 左右。 玩家 QQ 群人数多了,就会出现很多问题,很多人提各种建议,根本来不及做,只能先汇总下来,慢慢画饼。不过很多饼最终还是做出来了。另外,千万不要跟玩家硬杠,玩家觉得体验不好,自然有不好的道理,作为游戏开发者,要明白目标是什么,是赚钱(为爱开发,或者家里有矿的随便杠),一定要学会认怂。有一些玩家,玩的不爽了,没玩明白,反手就是 TapTap 上一个差评,而 TapTap 上的评评分,好敏感,可能几个差评就直接把分数拉下来了。所以,一定要学会认怂,和玩家硬杠,嘴上可能一时爽了,最终受损失的还是自己。另外,不要随便设置 QQ 管理员,设置了管理员,有一些管理员会觉得有权力了,但是有一些人不会真正站在游戏的角度考虑问题,会和玩家硬怼,因为管理员和游戏没有什么利益上的关系。玩家才不管这个人是不是游戏开发者,反手就是一个差评,受损失的还是游戏开发者。 为什么要以文字的形式来开发游戏呢?为什么不找人合作,将美术做的更好看一点呢?其实我也想啊,要是有一个亿,我也愿意组个团队去折腾,去尝试。可是现实是,对于独立开发者,最重要的还是要先活下去,有稳定的能够支撑基本生活的收入。文字游戏的开发周期,开发难度,是很小的,而文字的表现力,又是无限的。再就是找到合适的人,就很难,其次,风险很高,无法保证做出来一定有收入,还有分成问题,到底程序占多少,策划占多少,美术占多少,平均分配肯定是不合适的,还得根据不同的项目,工作量,来决定,前期分成如果没谈好,哪怕勉强做,可能后期效果,开发效率,也会有所影响。所以,并不是人越多,越好。 游戏从技术上采用了 xlua 热更新的方式开发的,所有的游戏逻辑都是用 lua 来写的,这带来了非常大的好处,我们可以快速的迭代版本,修改 Bug 等等。目前保持的节奏基本上是每天都会更新一点新内容。群里的玩家也养成了每天晚上8点等更新的习惯。如果技术上允许,建议大家使用这种方式,真的迭代很方便,资源上传 CDN 就可以。目前我用的 CDN 是 DogeCloud,新用户的话有免费的存储空间和流量,对于一个小项目来说,完全够用,目前我还没有把免费流量用完,连一半都没用上。 目前游戏的主要收益来自于两方面,一方面是广告,一方面是爱发电的赞助。至于赚了多少钱,从开发时间上来算,肯定没有之前打工时赚的多,不过还好有点收入。 游戏还在继续迭代,玩家的流量,也在缓慢减少,不过应该还能再持续一段时间。下一个项目,也在构思中了,敬请期待。 以上这些都是我从一个独立开发者的角度来谈的,无法和商业公司的开发模式去比较,仅仅是我所经历的一些东西。 技术交流,欢迎添加我的微信:ifloop

July 10, 2022 · 1 min · fred

让 Unity Shader 不受 Time.timeScale 的影响

Time.timeScale 是 Unity 的时间缩放变量。如果将此设置为0,那么 Time.time 将停止,并且 Physics 和 Animator 在默认情况下也将停止。所以,将 Time.timeScale 设置为0,通常为了做暂停相关的东西。 但是,有时候希望即使将 TimeScale 设置为 0,有些东西也能继续运行,比如在写 Shader 时,经常会用到 _Time 属性,但是它会受到 Time.timeScale 的影响。不过有以下几种方式可以做到不受影响。 方法一 private void Update() { material.SetFloat("_UnscaledTime", Time.unscaledTime); } 这种方式比较直接,但是也有弊端,如果要设置的 Shader 很多,会增加很多 MonoBehaviour。 方法二 Unity 可以设置 Shader 全局变量,即材质之间共享属性。 private void Update() { Shader.SetGlobalFloat("_UnscaledTime", Time.unscaledTime); } 这样场景中只需要一个做这件事情的 MonoBehaviour 的脚本即可。这个方法可以覆盖大多数情况。 方法三 如果场景中不想放任何脚本,还有一个更强大的方法,那就是使用 PlayerLoop 来独立于 MonoBehaviour 更新全局属性。 首先引入一个工具类 PlayerLoopModifier.cs,代码如下 using System; using System.Collections.Generic; using System.Linq; using UnityEngine.LowLevel; public class PlayerLoopModifier : IDisposable { private PlayerLoopSystem root; public PlayerLoopModifier() { root = PlayerLoop....

June 25, 2022 · 3 min · fred

Lua 带权重随机

t 是数据数组,weights 是权重数组,两个数组长度是一致的。 function RandomWithWeight(t, weights) local sum = 0 for i = 1, #weights do sum = sum + weights[i] end local compareWeight = math.random(1, sum) local weightIndex = 1 while sum > 0 do sum = sum - weights[weightIndex] if sum < compareWeight then return t[weightIndex], weightIndex end weightIndex = weightIndex + 1 end return nil, nil end

June 3, 2022 · 1 min · fred

理解 Entity Component System

Entity-Component-System (ECS) 是一种架构模式。这种模式广泛地应用在游戏开发中。ECS 使用组合原则,因此这种模式使程序具有更好的灵活性和扩展性。游戏场景中的所有对象都被视为一个实体 (Entity)。这种模式在默认情况下也具有更高的性能。 Entity-Component-System 有三个部分组成 Entity (实体) Component (组件) System (系统) 什么是 Entity Entity 可以理解为一个对象的标识,它没有任何具体的数据和行为,只是标识一个东西。在实现上,通常可以用一个 Struct 来实现。而组件,为其提供数据。 例如,我们要实现一个太空版本 Minecraft,所有游戏中能看到的,有形的东西,都算作一个实体。一搜飞船,一个角色等等。 什么是 Component 组件是附加到实体的可重用模块,它是实体的单一行为描述。组件提供了实体的表现,行为,和功能。不同组件的组合,可以创造出不同类型的逻辑实体。 什么是 System 一个 System 在运行时会遍历很多组件,以此实现高效的性能。例如渲染,物理,寻路。系统为组件提代了全局的管理和服务。 我们可以使用系统来分离逻辑和数据,系统可以用来处理逻辑组件,充当数据容器。 关于系统的例子 处理重力加速度 将速度应用到一个向量上 根据 AI 的设计,来控制机器人的输入 渲染 (位置,Sprite) 处理玩家输入 组合 我们可以组合不同的 Component,以及设置 Component 不同的数据值,来配置具体的实体。 ECS 的优势 降低代码量以及复杂度 对于逻辑扩展拥有很高的灵活性 对于 3D 和 VR 需要大量渲染逻辑的项目有性能优势 让非技术人员更方便地编写脚本 可以分离庞大复杂的类结构 代码可重用和可组合性很强 更加方便的单元测试 可以支撑复杂的 VR 程序 运行时组件的替换 多进程和多线程友好 分离数据和功能 更加灵活地定义游戏对象 提供了解耦,封装,模块化,可重用性方法,以此构成一个干净的设计。 ECS 的劣势 不能像 MVC 那样直观定义逻辑 要用好 ECS,需要更多的思考组件的设计 ECS 需要写大量的小型代码,增加了出错的风险 目前的应用没有面向对象广泛 ECS 示例 看下面的图,这是一个兔子的实体,其中有很多组件被附加到了实体上,Placeable、Huggable、Consumable、Hopping 等 …...

April 3, 2022 · 1 min · fred

Unity 完整的热更新方案和流程

在开发商业游戏时,热更新是一个很重要的模块,这里讲的热更新不是指仅仅修复Bug,而是进行游戏功能的更新。简单来讲,就是启动游戏后,跑个条,下载资源和代码,然后再进入游戏。本篇博客所写的内容并不是最优的解,只是完成了热更新这个事情而已,具体使用还需要使用者根据自己的项目来具体来看。 这里采用的方案是使用 AssetBundle 和 xLua。使用 AssetBundle 是为了资源的完全自主控制。而整个游戏的逻辑部分,则使用 xLua 来实现。当然,C# 的代码不可能一点没有,只是一些核心的功能模块,一般写好后就不会改变的东西,或者对性能要求很高的东西,放在 C# 就可以。 整个功能分为编辑器部分,和运行时部分。编辑器部分就是编 Bundle,生成版本文件等。而运行时部分就是从 CDN 下载版本文件,对比版本号及本地资源是否有要更新的,如果有,则更新,更新完后进入游戏。没有,则直接进入游戏。 编辑器部分 编辑器部分主要就是生成 Bundle 文件,首先,我是按目录来划分 Bundle 的,任何一个目录下的文件(不包括子目录)则会打成一个 Bundle。例如下面的目录结构 Res/ - ConfigBytes/ - UI/ - LuaScripts/ - Data/ - ItemsData/ - CharactersData/ 首先 Res 目录是资源的主目录,下面有各种子目录(Res 目录下不会有需要打 Bundle 的文件)。ConfigBytes 目录下的文件,会打成一个 Bundle。UI 目录下的文件会打成一个 Bundle。LuaScripts 目录下的文件会打成一个 Bundle。Data 目录下的 ItemsData 目录中的文件会打成一个 Bundle,Data 目录下的 CharactersData 目录会打成一个Bundle。如果 Data 目录下存在文件(非目录),则这些文件会打成一个 Bundle。简单来讲,就是会按文件夹来决定哪些文件打成一个Bundle,检查的时候只会取一个文件夹下的文件,而不会递归取这个文件夹下的子目录。 LuaScripts 目录在开发时会放在 Assets 目录外面,与其同级,在编 Bundle 时,会拷贝 LuaScripts 目录及下面的所有文件,按原有目录结构,拷贝到 Res 目录中,并且会将每一个 xxx.lua 文件的扩展名改为 xxx....

March 20, 2022 · 14 min · fred