噪声函数与地图生成

在我学习声音信号处理的时候,我的大脑很自然地联想到了地图生成。这篇博客记录了关于信号处理的一些概念与地图生成相关的东西。这些知识点不是一些新的东西,但对我来说,是以前从未接触过的,所以我想记录一下,并且分享给大家。这篇博客会覆盖一些简单的主题,频率、振幅、噪声的种类、噪声的应用等。涉及到的数学部分,基本上只有正弦波形。 这里会从简单的概念开始,然后逐渐深入。 注意:下面涉及的代码,虽然是以 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 · 猫猫

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 · 猫猫

一个独立游戏的开发总结

从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 · 猫猫

让 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 · 猫猫

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 · 猫猫

理解 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 · 猫猫

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 · 猫猫

一个游戏兑换码生成及验证方案

在开发独立游戏《小镇危机:来自丧尸的问候》时,需要设计一个兑换码的功能,但是在网络上也没有找到合适的方案,所以这里就自己考虑了一种。这里会说明思路,具体的实现,可以按照自己的方式去定制。 我们的游戏中有两种资源可以兑换,一种是星星,一种是钻石。所以就需要在兑换码中通过某种方式表示出来。本质上就是将自己想兑换的数据,通过某种方式的变换,隐藏在兑换码中,然后服务器是知道解码方式的,通过解码,即可还原出玩家兑换的是什么,然后将兑换物品回给玩家。 我们的游戏兑换码为14位的,里面包含的信息有兑换类型,兑换数量,这两个信息,例如 LMA60XV7380QBH。 1. 兑换码生成和验证过程 兑换码由大写字母和数字组成,对于兑换码中的每一个字符,我们可以使用的字符为 [0, 9] 和 [A, Z]。 确定要表示的信息,所占用的字符数。对于我们的游戏来说,兑换类型只需要 1 个字符即可,而兑换数量,需要占用 4 个字符,到此,已经消耗掉 5 个字符。 兑换码的数据校验和,会占用2个字符,到此消耗掉 7 个字符。 剩下的 7 个字符,为唯一的随机字符串,用于填充验证码的。 根据上面的步骤,首先生成第 3 步所需要的 7 位随机字符串。由于我们每一位可用的字符数是36个,所以最终的排列组合有上百亿种,我们不需要那么多,几百万个足够了。这里要注保证唯一性。生成的字符串如下所示 DLDWVEQ WM8YB8M MNUP5RR 3RG7X8D VUBPD8J J3L3ZR1 1Y1ALB3 R6PRATR 然后表示出兑换码要兑换的数据,假设这里要兑换星星,并且兑换的数量为 10000 个。首先使用一个字符来表示星星,例如使用 S 来表示,当然,也可以将 36 个字符进行分组,然后从组中去随机一个,这样更不容易被破解。然后兑换的数量,可以转为 16 进制,或者 34 进制都行。假设这里使用 34 进制来表示,10000 转为 34 进制就是 8M4,不足4位,前面进行补0,也就是 08M4。进制转换的逻辑需要自己写。 取一个随机字符串,与要兑换的信息进行组合,组合的方式随意,只要最后恢复时使用相同的方式即可。假设这里将随机字符串的前3位放在兑换码的开始,然后跟上兑换类型,后三位放在最后。这里使用上面第一个随机字符串为例子。经过这一步组合,得到的字符串如为 DLDS08M4WVEQ。 然后计算上面得到的字符串的校验和,为 AD,将 AD 放在最后,这里随意,可以按自己想放的位置去放,自己知道就好了。现在得到的字符串为 DLDS08M4WVEQAD。 最后需要做的就是将上面得到的字符串进行打乱,按什么样的方式打乱呢,这里需要事先生成几套排列方案,例如 [8, 1, 5, 9, 2, 4, 0, 12, 7, 10, 13, 11, 3, 6],也就是字符中第一个字符放到索引 8 的位置,第二字符放到索引 1 的位置,第三个字符放到索引 5 的位置,以此类推。我们可以生成多套这样的排列方案,然后使用字符串中的其中一位,来表示,例如我们使用第2位的L来表示使用哪一套方案,将 L 转换为 int 数值,然后对排列套数进行 % 操作,得到使用哪一套排列方案,进行排列。这里要注意,第2位不能变,也就是不受随机排列影响,忽略掉,否则服务器没法恢复。...

March 1, 2022 · 1 min · 猫猫

在过去的一个月里

已经一个多月没有写 This Week In Moeif 的周记了,在过去的一个月里,发生了很多事情。年前我们早早的回家了,不过游戏开发的工作依然在不断进行着。年后也过了正月十五才回的上海。 从做独立开发者开始,生活就没有了假期与工作日的明显界限,因为时间都是自己的,做的事情也是自己想做的,并且所做的一切,也都是自己的积累。有时候也会忙到很晚,但是明显没有打工时的那种身心疲惫。 1. 游戏码农:那些打工的日子 在过去的一个月里,首先我们上线了《游戏码农:那些打工的日子》这个游戏。并且这个游戏已经有了广告收入,虽然不多,但是已经开始有了。2月份的收入如下图 这个游戏可以目前安卓和iOS版本都已上线,可以直接在Taptap,好游快爆,AppStore等搜索 游戏码农 即可找到,也可以直接从下面连接进入应用平台。 Appstore: https://apps.apple.com/cn/app/id1607035933 TapTap: https://www.taptap.com/app/230651 好游快爆:https://www.3839.com/a/141332.htm 2. 小镇危机:来自丧尸的问候 昨天,我们又上线了另一个游戏《小镇危机:来自丧尸的问候》。这个游戏的资源包是买的,但原版是英文版,我们自己改成了中文版,然后加入了排行榜系统,兑换码系统。目前游戏也已在安卓和iOS平台上线。 AppStore: https://apps.apple.com/cn/app/id1611293156 TapTap: https://www.taptap.com/app/231533 好游快爆:https://www.3839.com/a/141899.htm 3. 接下来的规划 就在今天,我们确定了接下来要开发的一个项目,我们乐观的预计,会比前两个游戏拥有更多的用户量,以及更多的收入。敬请期待。 4. 另外欢迎加入我们的 Moeif Games 玩家交流群:163359029 猫语互动 欢迎关注微信公众号 猫语互动,博客文章同步推送

February 28, 2022 · 1 min · 猫猫

Roguelike In Rust 05: 生成地牢

这一节我们将会创建一个地牢,地牢由不同的房间组成,房间与房间之间是联通的,玩家可以行走。下面是最终运行效果图。 ...

February 22, 2022 · 4 min · 猫猫