上一节,讲了游戏从启动到登录页,经历的过程。
这一节,讲下我们在游戏界面的那些元素,从美术打开PS开始,是一步步怎么样出现到我们的游玩界面中的。
美术资源有很多种类型,游戏资源类型, ui、音效、模型、shader、icon、字体、场景、特效
先简介一下它们分别是什么:
即UserInterface, 是玩家在游戏中看到的一个个界面模块。
比如背包界面、登录界面、角色界面、技能界面等等,也是在游戏开发中改动、更新比较频繁的模块。
在打包时,
也是比较复杂的模块。要处理依赖关系等。
格式:prefab,依赖png
即玩家在游戏中主动触发的交互声音,及背景音乐等。
比如进游戏后,需要播放背景音乐。 点击按钮时,播放点击音效。放技能时,播放技能音效等。
通常声音分为两种类型进行处理。一种是即时音效。就是交互一次,播放一次的类型,这种声音的特点是时长短。另一种是背景音乐。需要常驻在内存中,进行循环播放。
在打包时,
比较简单,没有依赖关系。
格式:mp3\wav
通常是指3d游戏中,那些角色、npc、怪物等模型。
在mmo中用得最多,很多任务等,都要推进对应的npc,击杀对应的怪物,这些都需要和模型支撑。
在打包时,
模型资源身上的依赖,如材质、贴图、fbx、模型动画等,都需要计算。
格式:prefab,依赖png,fbx,mat,animation
着色器。可以用来做很多视觉效果,特效中用得比较多。
shader的过程,是属于gpu模块。其中每个在屏幕上显示的元素,都需要经过shader的函数渲染。所以可以通过调整shader来达到各种特殊的视觉艺术效果。如镜面反射、半透明、被遮挡后的描边等。
shader的打包,
也比较简单,因为没有依赖到其它信息,且本身占用很小,所以通常可以在游戏开始时,加载常驻到内存。
格式:shader
在界面上,会看到各种图片,人物图片、物品图片、按钮图片等。
图片是游戏中最常见的元素之一。
icon图片的打包,
也比较简单,因为没有依赖到其它资源。但有一点要注意,因为icon会被频繁的展示与隐藏,所以需要做引用计数,当长时间不用某icon时,才将其卸载。以免频繁加载卸载导致内存压力。
格式:png
在界面上会看到很多文字,介绍、对话之类的文字信息。
文字需要字体才能显示的,在unity字体相当于一张材质球,该材质中的贴图,该贴图中记录了大部分常用文字。
当需要显示某个文字时,在该贴图找到对应的图片信息部分显示出来,就有了游戏中的文字。
打包时,
字体本身没有依赖到其它的信息,所以打包也比较简单。
格式:ttf
场景的概念,类似关卡。也就是玩家所处的一个场景空间。
当游戏进行到某种情况下,需要进入另外的故事,就需要跳转场景。
场景本身会依赖很多种信息,最主要的是关照信息。需要提前烘焙好。
在加载场景时,去加载烘焙信息。
打包时,
需要处理依赖,比较复杂,过程也比较久。
格式:scene
特效是游戏中那些通常来说比较漂亮的效果,比如发光的球体,闪烁的提示,物品获得时的跳动效果等。
特效通常依赖也比较多,比如材质、贴图、shader、动画文件等。
打包时,需要处理上述依赖。
格式:prefab
上面介绍了常见的需要打包的游戏资源,某些资源也是有用到,却没有单独提出来打,比如动画资源、材质球等。
这是有原因的。
先说下动画资源为什么没打包
动画资源通常是和模型或者特效资源一对一绑定的,也就是说,一个动画文件并不会被用于多个对象中。
所以也用拆分开来打,在打包特效、或者动画时,连同它们一起打包即可。
在加载时,也一样,加载该特效包后,先加载其中的动画,再加载其中特效即可。
再说下材质球为什么没打包。
因为材质球本身是对shader提供的参数支持,类似一个配置文件。它需要用到哪个贴图,应用哪些参数,进行一个怎样的着色过程。这样的一个描述。
所以既然已经单独打包了shader和贴图,那么材质球可以转换成一份配置文件,也就是json文本。
当模型或者特效打包时,这份json文本也随之进行打包到对应的目录。
当加载时,也从这里面去读取配置,并创建材质球去应用这些配置。
相比较而言,字体、shader、音效等不依赖其它资源而存在资源,会被进行打包,是因为可能会被多个其它资源引用到。
并不便于随依赖它的资源一起打包。否则造成打包了多份相同的资源,造成冗余。
上面,我们讲完游戏中有哪些常用资源。
游戏并不是一个部门完成的,通常至少需要三种岗位——策划、美术、程序。
策划在策划的工具流中提出需求,需要哪些美术资源,及配置游戏资源,决定哪里需要用到哪些资源
工具流中通常有excel配置表、mysql调参数、后台该参数等
美术在美术流工具中创造游戏资源
工具流中通常有ps做贴图、unity做特效和场景、maya做模型等
程序在程序工具中,用代码实现资源显示在游戏中
工具流中通常有unity搭建ui界面、lua实现客户端功能、c++实现服务器功能等
所以,游戏资源最开始是从美术人员,先开始创作的。让我们从这里开始。
美术人员按资源细分,又分为场景、ui贴图、原画、模型、特效、动作等。
如果是2D游戏,修改比较多的是,如ui界面、以及界面上需要的图集、icon、2D特效需要替换。
如果是3D游戏,可能模型、场景、3D特效之类的,也比较多。
再有就是一些通用的,字体、shader、音效这些那些,美术制作的资源。
它们都是经过了怎么样的流程,最终得以在游戏界面上显示的?
这也是一个比较复杂的过程。先说四个的思路过程:
A、同步:各种资源,是在美术的工作目录中制作的,需要先将同步美术资源至程序员的目录下,让程序员才能用起来。
B、打包:各种资源,根据类型的不同、依赖的不同,打包资源的方式也不同
C、发布:各种资源,根据需要发布的平台不同,需要打成不同平台资源,几个平台就打几份,因为每个平台读取的资源格式不一样
D、加载:在运行游戏时,需要调用对应的加载方法,将资源加载出来,才能在游戏中展示。
同步前言说明
!注意:虽然大体上都是这四步,但不同类型的资源走的过程会有区别。
美术与程序的产出环境也不同。
美术是在美术目录提交,
程序生产资料是在程序目录提交(通常是游戏引擎目录下)。
所以美术的资源做好后并不会直接能让程序使用。需要有专门的”同步“操作,将美术资源同步至程序目录。
而根据美术资源的不同,如icon、原画等资源,是在ps中制作的,无须用到游戏引擎。
这类资源在原理上并不复杂,没有资源的依赖,通常拿来就能使用,不会存在ps中能显示,但导入Unity后就显示异常的效果。
不需要经过特殊检查。
所以这类不需要引擎工具去同步的资源,
包括:字体、icon、shader、音效。
这类资源,手动由程序员去拷贝至程序项目路径。
这里共有四种资源,
还剩下另外四种,其中的ui资源,是由程序在引擎中制作,所以也不存在同步。
于是剩下场景、特效、模型,需要经过特殊同步。
同步具体过程
同步并不是简单的将从美术的资源目录,拷贝至程序的资源目录。
需要根据资源的类型,进行该类型的检查。
A拿场景同步举例:
A美术做好场景资源,提交美术svn
A 在后台web界面,点击”同步场景资源“
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VpFEEfOW-1651915156021)(en-resource://database/5083:1)]
A web发送php指令,在数据库插入”场景待打包“的一条新数据,状态设置为”0“,表示待打包
$sql = "insert into foreign_asset_build_list ( add_time, type, system, language, comment, status) values ( now(), '" . $type . "', '" . $system . "', '" . $language . "', '" . $comment . " ', 0);";
A web后台Python服务,每5秒,定时轮询,查询该数据库中,是否有状态为”0“的待打包的数据。
A将其状态改为”1“,表示执行中
update_sql = f"update {db_table} set status = 1 , begin_build_time = {nowtime} "\
f"where id = {async_event_context.event_id}"
A从数据库查询该数据的人完整信息,如平台类型、资源类型等。
A先对资源初始化, svn clean Up, svn revert -R, svn up
await shell_command(f"svn cleanup {self.project_path}/Assets/_Resource")
await shell_command(f"svn revert -R {self.project_path}/Assets/_Resource")
await shell_command(f"svn up --force --accept theirs-conflict {self.project_path}/Assets/_Resource")
A根据资源的类型,定义需要拷贝的源路径和目标路径,也就是artPath和programPath
copy_art_path = f"{self.game_art_path}/Assets/{self.asset_types['scene']}/{self.resource_path}/components"
copy_program_path = f"{self.project_path}/Assets/_Resource/components"
A然后shell指令,先删除清空目标路径
cmd = f"rd /s/q {target}"
await shell_command(cmd)
subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
A再拷贝路径文件
cmd = f"xcopy /e/y/i/d/r {source} {target}"
await shell_command(cmd)
A再使用子进程,调用Unity中自定义的检查资源的函数
check_command = f"{UNITY_PATH} -executeMethod AssetsUpgradeCheck.CheckMissSceneRes -quit -batchmode " \
f"-logFile {scene_check_log_file} -projectPath {self.project_path}"
child = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = child.communicate()
A 在该函数中,Unity将读取场景组件中每一个材质球,进行检查
读取材质球
Material mat_file = AssetDatabase.LoadAssetAtPath(file, typeof(Material)) as Material;
检查看它的材质球是否为空
if (mat_file == null)
检查是否包含了错误ErrorShader
else if (mat_file.shader.name.Contains("ErrorShader"))
检查贴图是否为空
if (mat_file.mainTexture == null)
序列化资源
SerializedObject psSource = new SerializedObject(mat_file);
根据参数,获取其属性值
SerializedProperty emissionProperty = psSource.FindProperty("m_SavedProperties");
来判断
texture2d = (Texture2D)psSource.objectReferenceValue;
A再将上面如果有错误的信息(如没有贴图),写入到错误文件中
CreateTargetFolder
FileStream prefab_file = new FileStream("scene_check_file/miss_res.log",
FileMode.Create);
StreamWriter prefab_stream = new StreamWriter(prefab_file, Encoding.UTF8);
Debug.Log("Count:" + miss_list.Count);
string context = stringBuilder.ToString();
prefab_stream.Write(context);
prefab_stream.Flush();
prefab_stream.Close();
prefab_file.Close();
AUnity中执行就结束了,现在python中检查刚才的文件,如果内容为空,说明没有错误。否则就将错误打印出来给用户。
miss_log = open(program_check_miss_data, 'r', encoding=self.coding_rule).read()
A如果检查都没有错误,再执行上面的”同步资源“的过程。
A然后再subprocess.Popen,执行svn的提交
await shell_command(f"svn st {self.project_path}/Assets/_Resource | for /f \"tokens=1,2\" %i in ('findstr /B \"?\"') do svn del %j")
await svn_add(f"{self.project_path}/Assets/_Resource")
await svn_submit_files(f"{self.project_path}/Assets/_Resource", f"{self.async_event_context.event_parameters['language']} "
f"sync {self.async_event_context.event_parameters['type']} "
f"sync id {self.async_event_context.event_id}")
到这里,美术资源就从美术工作目录同步至了程序目录。
下一步,程序将资源打包成.unity3d后缀的assetBundle资源,使之可以热更新。
这里拿音效资源举例:
@@音效相关人员,找到需要的音效资源,在对应svn目录提交,
通常格式mp3、wav两种程序员更新资源下来,并统一放在音效目录调用引擎工具,打包音效的命令在自定义的编辑器工具界面中,
程序更新下来,点击”打包sound资源“
读取路径 Assets/Resource/sound“FileHelper.FindFileBySuffix,
通过后缀,检索其下的.mp3格式的全部文件,对资源的信息,生成md5。
并读取资源旧的信息中记录的md5。
进行对比,(为了减少打包需要的时间,只对有改动的部分打包)
如果相同,则该资源跳过打包。
如果不同,则说明是新的资源,需对其设置ab名及后缀名。
AssetImporter assetImporter = AssetImporter.GetAtPath(sound);
assetImporter.assetBundleName = AssetsBuildTool.OsType + "/" + sound.Replace("Assets/Resource/", "").Replace(".mp3", "") + NewLastName;
assetImporter.assetBundleVariant = BuildStaticCommon.ASSETS_END_EXTEND;
BuildPipeline.BuildAssetBundles(outPath, BuildAssetBundleOptions.ChunkBasedCompression | BuildAssetBundleOptions.DeterministicAssetBundle, EditorUserBuildSettings.activeBuildTarget);
string [] assetBundleManifestAssetBundles = assetBundleManifest.GetAllAssetBundles();
manifest是unity打包时,生成的记录资源依赖关系的文件因为有些资源比较复杂,会引用到其它的资源。需要先加载出其它资源,再最后加载出本体,才正常。所以需要依赖文件,当加载某个资源时,先加载出其依赖的资源。
string[] assetBundleManifestDependencies = assetBundleManifest.GetAllDependencies(assetBundleManifestAssetBundles[i]);
再ftp同步至alpha版本(内部开发测试环境)
再从alpha版本更新至center(内部多分支环境)
再从center发布至外网(对外部玩家的最终,这个需要运营在更新时让后台进行发布)
发布的版本,还分客户端和服务端。可分别发布。
到这里发布就算完成了。
如果玩家是之前下载的手机包,有了新版本后,有了新版本后,
即可通过热更新的方式,只更新那一部分数据,而不用重新安装应用包。即:热更。
那此时,最新的游戏数据,还在服务器上。
当玩家启动游戏时,就会执行检查,(可参考我的上一个视频从启动到登录)。
通过platform的平台配置,用其中的php向服务端发送请求,
根据玩家本地的资源版本,与服务器的资源进行比对,如果版本不一,则执行更新流程。
将有差异的文件下载到本地,替换原来的资源。
即完成资源更新。
this.mAssetBundle =
AssetBundle.LoadFromFile(resLoadInfo.GetPathForLoadfromfile());
结束
遗留问题:
只讲到了音效的打包过程,ui的加载过程。
但其它的资源如场景、模型、特效等等资源的打包与加载的过程没讲。
各种资源的方式其它都是不同的
那下一篇我们再细讲这个。