最近看到了很多Unity的游戏工程开始往Godot迁移,具体原因是因为Unity的收费政策的调整导致很多开发者不愿意继续使用Unity,逐渐开始跳转到其他的游戏引擎/框架上,我所见的就C#而言,跳的大多是Godot Mono,不过我正好近期也在用Godot Mono做一个解密游戏。
当然C#而言也有其他的选择,2D的有XNA的复刻版Monogame和FNA,3D有Flax、Orge或是Stride。
不过在迁移游戏的时候,有一点比较重要的是,有的代码因为过于依赖引擎/框架的API,需要做出大量改动,或者是不知道要怎么改才能迁移过去。这一点在早期开发游戏的时候就要把纯粹的逻辑部分和调用引擎/框架的API分开来,在这里我要介绍一个无论是否要迁移游戏,或者是正常开发,都很重要的一个程序思想。正如标题所说,前后端分离。
这样做有利于把逻辑部分和图形分开来,一方面有利于分工合作,另一方面整个项目不会太过凌乱,而且对于比较程式化的游戏而言,这种实现又是不得不需要的。
说一下最近看到的两个例子:
杀戮尖塔 #
我个人比较喜欢这个游戏,之前在和哥们联机的时候有留意一下Console的输出,竟然发现了游戏的地图生成是前后端分离的。一开始我看那个地图生成以为是一个点生成完后往上移动多少个像素再生成一个点,然而并不是这个样子。
14:05:48.800 INFO dungeons.AbstractDungeon> INIT CARD POOL
14:05:48.801 INFO helpers.CardLibrary> [INFO] Adding blue cards into card pool.
14:05:48.802 INFO dungeons.AbstractDungeon> COLORLESS CARDS: 35
14:05:48.802 INFO dungeons.AbstractDungeon> CURSE CARDS: 10
14:05:48.804 INFO dungeons.AbstractDungeon> Cardpool load time: 4ms
14:05:48.805 INFO unlock.UnlockTracker> Already seen: AscendersBane
14:05:48.805 INFO unlock.UnlockTracker> Already seen: Strike_B
14:05:48.805 INFO unlock.UnlockTracker> Already seen: Strike_B
14:05:48.806 INFO unlock.UnlockTracker> Already seen: Strike_B
14:05:48.806 INFO unlock.UnlockTracker> Already seen: Strike_B
14:05:48.806 INFO unlock.UnlockTracker> Already seen: Defend_B
14:05:48.806 INFO unlock.UnlockTracker> Already seen: Defend_B
14:05:48.806 INFO unlock.UnlockTracker> Already seen: Defend_B
14:05:48.806 INFO unlock.UnlockTracker> Already seen: Defend_B
14:05:48.806 INFO unlock.UnlockTracker> Already seen: Zap
14:05:48.806 INFO unlock.UnlockTracker> Already seen: Dualcast
14:05:48.806 INFO basemod.BaseMod> postCreateStartingDeck for: DEFECT
14:05:48.806 INFO basemod.BaseMod> postCreateStartingDeck adding [ AscendersBane Strike_B Strike_B Strike_B Strike_B Defend_B Defend_B Defend_B Defend_B Zap Dualcast ]
14:05:48.815 INFO dungeons.AbstractDungeon> Content generation time: 52ms
14:05:48.815 INFO basemod.BaseMod> publishStartGame
14:05:48.815 INFO basemod.BaseMod> mapDensityMultiplier: 1.0
14:05:48.815 INFO basemod.BaseMod> publishStartAct
14:05:48.816 INFO basemod.BaseMod> publishPostDungeonInitialize
14:05:49.149 INFO scenes.AbstractScene> Fading in ambiance: AMBIANCE_BOTTOM
14:05:49.326 INFO dungeons.AbstractDungeon> Note For Yourself is disabled beyond Ascension 15+
14:05:49.327 INFO EventUtil> Adding conditional SpecialOneTimeEvents.
14:05:49.327 INFO EventUtil> Checking for SpecialOneTimeEvent replacements...
14:05:49.333 INFO dungeons.AbstractDungeon> Generating Room Types! There are 57 rooms:
14:05:49.334 INFO dungeons.AbstractDungeon> SHOP (5%): 3
14:05:49.334 INFO dungeons.AbstractDungeon> REST (12%): 7
14:05:49.334 INFO dungeons.AbstractDungeon> TRSRE (0%): 0
14:05:49.334 INFO dungeons.AbstractDungeon> ELITE (8%): 7
14:05:49.334 INFO dungeons.AbstractDungeon> EVNT (22%): 13
14:05:49.334 INFO dungeons.AbstractDungeon> MSTR (53%): 27
14:05:49.340 INFO map.RoomTypeAssigner> #### Unassigned Rooms:
14:05:49.342 INFO map.RoomTypeAssigner> class com.megacrit.cardcrawl.rooms.RestRoom
14:05:49.342 INFO map.RoomTypeAssigner> class com.megacrit.cardcrawl.rooms.RestRoom
14:05:49.342 INFO map.RoomTypeAssigner> INFO: Node=(2,13):[(2,14)] was null. Changed to a MonsterRoom.
14:05:49.342 INFO map.RoomTypeAssigner> INFO: Node=(6,13):[(5,14)] was null. Changed to a MonsterRoom.
14:05:49.342 INFO dungeons.AbstractDungeon> Generated the following dungeon map:
14:05:49.342 INFO dungeons.AbstractDungeon>
/ / | \
14 R R R R
/ | \ \
13 M M M M
| \ / \ \ /
12 M ? $ R
\ \ \ | \
11 E M ? M M
/ / \ / / /
10 M ? R ? E
| | \| |
9 M ? E ?
\ \ / | \ \
8 T T T T T T
\|/ / / |
7 R E M M
| \|/ /
6 M ? R
| \| \ \
5 E R E E
\| \ |
4 ? $ ?
| \ / \|
3 M M ?
/ | | \ /
2 M ? M $
\ \ \| \
1 M ? M ?
/ / / |
0 M M M M
14:05:49.343 INFO dungeons.AbstractDungeon> Game Seed: 6166791066988275480
14:05:49.343 INFO dungeons.AbstractDungeon> Map generation time: 16ms
14:05:49.343 INFO dungeons.AbstractDungeon> [INFO] Elite nodes identified: 7
14:05:49.343 INFO dungeons.AbstractDungeon> [INFO] Emerald Key placed in: [5,10]
这些大概是我在控制台看到的样子,我截取了部分有价值的信息,这里的输出包括了卡牌的生成以及地图的生成,它能输出到Console里面,说明这个地图生成有一定的数据结构,是一个非离散的网状空间,从输出而言看着像某一种树,不过这种地图生成算法我没有研究过。
推测在生成完实际的地图之后,再返回到图形渲染去生成玩家能够点到的图标,之后再返回进入战斗场景的数据。
其实在Traditional Roguelike中,因为游戏以Grid-Based著称,所以地图生成这一块基本需要用到一个二维数组。
接下来要介绍另一个游戏,它是个Traditional Roguelike。
Caves of Qud #
Caves of Qud很不幸是Unity做的,他们团队在Unity刚出事的时候就说了要用三个月的时间把游戏从Unity迁移到Godot上,几天前刷Twitter的时候,发现他们已经把Core迁移过去了。视频和截图都是纯ASCII,并且是在终端输出。这其实说明了一点,Caves of Qud也采用了前后端分离的设计思路。
虽然Caves of Qud的特效做的很绚丽,贴图也做的很好,但是这背后的程序化工作是少不了的。
这边贴一下Brian Bucklew在Twitter发的迁移工作视频,有条件可以去看看,没想到包括GUI界面都做了ASCII:https://x.com/unormal/status/1703643790047592912?s=20
还有个不一定能看到的图片:
Bucklew展示的截图和视频完全是游戏里面所表示的那样,只不过是一个没有贴图和美化的版本,这些纯ASCII依然具有很强大的魅力。
而且,做这些巨量代码的迁移工作只花了他差不多一天的时间,仅仅只是复制粘贴而已,这些后端部分的代码不敢说全部,但至少绝大部分不依赖Unity,因为它们不依赖图形渲染,它们只是一堆随时能被调用的数据。
我自己做的很多游戏也遵循了前后端分离的思路,地图生成,敌人的行为,地图的数据。但是目前没有做到完全分离,有时候处理逻辑上的东西也有用到引擎自带的东西。
但是不得不承认这是非常需要学会的,而且作为一个Programmer,这也是一门必修课。