这里选取 杀戮尖塔2 的游戏解包内容, 用于借鉴其代码结构和目录架构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149
| STS2SourceCode/ ├── project.godot # Godot4 主工程配置, 导入后加载整个项目 ├── .godot/ # Godot引擎缓存目录(导入资源生成) ├── src/ # 全部C#业务源代码, 游戏核心逻辑 │ ├── Core/ # 领域核心逻辑, 纯数据层, 与渲染解耦 │ │ ├── Models/ # 全部实体数据模型定义 │ │ │ ├── Cards/ # 卡牌基类、各类卡牌实现、卡牌升级、附魔逻辑 │ │ │ ├── Relics/ # 遗物基类、全部遗物效果定义 │ │ │ ├── Potions/ # 药水实体、药水使用逻辑 │ │ │ ├── Characters/ # 玩家角色基础属性、职业数据 │ │ │ ├── Monsters/ # 怪物模板、AI行为模板、怪物属性 │ │ │ ├── Buffs/ # 力量/减益状态、持续效果、状态标记 │ │ │ ├── Events/ # 地图随机事件数据模型、事件选项 │ │ │ ├── Enchantments/ # 二代新增卡牌附魔数据结构 │ │ │ └── Collections/ # 牌库、手牌、弃牌堆、墓地通用容器 │ │ ├── Combat/ # 战斗核心驱动 │ │ │ ├── CombatState.cs # 战斗全局状态容器(核心类) │ │ │ ├── TurnSystem/ # 回合流程:回合开始、结束、玩家/怪物行动阶段 │ │ │ ├── CardActions/ # 卡牌动作执行管线、链式效果处理器 │ │ │ ├── DamageCalculation/ # 伤害公式、易伤、护盾、抗性计算逻辑 │ │ │ ├── Graveyard/ # 墓地独立模块, 死灵绑定者核心机制 │ │ │ └── CombatTriggers/ # 战斗各类钩子(出牌时、受伤时、死亡时等回调) │ │ ├── Map/ # 爬塔大地图逻辑 │ │ │ ├── MapGenerator/ # 随机地图路径生成算法 │ │ │ ├── MapNodes/ # 节点类型:战斗、商店、篝火、事件、宝箱、休息点 │ │ │ ├── RouteLogic/ # 路线筛选、可达节点判定 │ │ │ └── MapReward/ # 节点通关奖励规则 │ │ ├── RNG/ # 随机数系统 │ │ │ ├── SeedManager.cs # 全局种子管理 │ │ │ ├── Xoshiro256.cs # 底层随机算法实现 │ │ │ └── RandomScope/ # 隔离随机域, 保证对局可回放 │ │ ├── Save/ # 存档持久化 │ │ │ ├── SaveData/ # 存档数据序列化模型 │ │ │ ├── SaveLoader.cs # 读档逻辑、数据兼容修复 │ │ │ └── SaveWriter.cs # 实时存档写入 │ │ ├── Multiplayer/ # 多人联机模块(二代原生支持) │ │ │ ├── NetworkSync/ # 战斗状态同步器 │ │ │ ├── Lobby/ # 联机大厅、房间创建、玩家加入 │ │ │ ├── NetworkMessages/ # 网络通信数据包定义 │ │ │ └── CoopCombatRules/ # 多人合作战斗特殊规则 │ │ ├── Localization/ # 多语言本地化 │ │ │ ├── LocaleManager.cs # 语言切换管理器 │ │ │ ├── TextFormatter.cs # 文本变量插值、动态数值替换 │ │ │ └── LocaleKeys/ # 全局文本常量Key定义 │ │ └── Modding/ # 原生Mod加载框架 │ │ ├── ModLoader.cs # Mod Pck包加载器 │ │ ├── ModEvents.cs # 全局事件钩子系统 │ │ ├── ModApi/ # 对外暴露Mod开发接口 │ │ └── ModConfig/ # Mod配置读取与校验 │ ├── UI/ # 视图层, 所有界面交互逻辑(Godot节点绑定脚本) │ │ ├── CardViews/ │ │ │ ├── CardRenderer.cs # 卡牌基础渲染 │ │ │ ├── CardTooltip.cs # 卡牌悬浮提示面板 │ │ │ ├── GraveyardView.cs # 墓地卡牌预览界面 │ │ │ └── CardUpgradePopup.cs # 卡牌升级弹窗 │ │ ├── CombatUI/ │ │ │ ├── PlayerStatusBar.cs # 血量、护盾、金币显示 │ │ │ ├── BuffIconPanel.cs # 力量/减益图标渲染 │ │ │ ├── DeckPileView.cs # 牌库、弃牌堆鼠标悬浮预览 │ │ │ └── MonsterUI.cs # 怪物血条、怪物状态面板 │ │ ├── RewardScreens/ │ │ │ ├── CardRewardScreen.cs # 战斗后卡牌三选一界面 │ │ │ ├── RelicRewardScreen.cs # 遗物奖励界面 │ │ │ ├── PotionRewardScreen.cs # 药水奖励界面 │ │ │ └── CampfireScreen.cs # 篝火休息、升级、移除卡牌界面 │ │ ├── ShopUI/ │ │ │ ├── ShopItemPanel.cs # 商店商品渲染 │ │ │ ├── PurgeCardScreen.cs # 商店移除卡牌界面 │ │ │ └── ShopRefreshLogic.cs # 商店商品刷新规则 │ │ ├── MapUI/ │ │ │ ├── MapNodeRenderer.cs # 地图节点图标绘制 │ │ │ ├── RouteLineRenderer.cs # 路线连接线渲染 │ │ │ └── MapNavigation.cs # 节点点击跳转逻辑 │ │ └── MenuUI/ │ │ ├── MainMenu.cs # 主菜单 │ │ ├── CharacterSelect.cs # 角色选择界面 │ │ ├── SettingsMenu.cs # 图像、音频、控制设置 │ │ ├── SaveSelectScreen.cs # 存档列表选择 │ │ └── MultiplayerLobbyUI.cs # 联机大厅界面 │ ├── Audio/ # 音频控制系统 │ │ ├── AudioManager.cs # 全局音频单例、音源池管理 │ │ ├── SFX/ │ │ │ ├── CombatSFX.cs # 战斗音效调度 │ │ │ ├── CardSFX.cs # 抽牌、打出、丢弃卡牌音效 │ │ │ └── UISFX.cs # 点击弹窗通用音效 │ │ └── Music/ │ │ ├── MusicTrackManager.cs # BGM切换控制器 │ │ ├── CombatMusic.cs # 战斗背景音乐 │ │ └── MapMusic.cs # 地图背景音乐 │ └── Animation/ # 动画、视觉特效逻辑 │ ├── CardAnimations/ │ │ ├── DrawCardAnim.cs # 抽牌动画 │ │ ├── PlayCardAnim.cs # 打出卡牌动画 │ │ └── DiscardCardAnim.cs # 卡牌丢弃动画 │ ├── CombatAnimations/ │ │ ├── MonsterAttackAnim.cs # 怪物攻击动画 │ │ ├── HitAnim.cs # 受伤抖动、受击动画 │ │ └── DeathAnim.cs # 怪物死亡退场动画 │ └── Vfx/ │ ├── ShieldVfx.cs # 护盾粒子特效 │ ├── ElementVfx.cs # 灼烧、冰冻、毒素特效 │ └── GenericParticleVfx.cs # 通用粒子特效控制器 ├── scenes/ # Godot场景文件 *.tscn 节点树资源 │ ├── CombatScenes/ │ │ ├── CombatMain.tscn # 战斗主场景根节点 │ │ ├── PlayerHandContainer.tscn # 手牌容器节点 │ │ └── MonsterArena.tscn # 怪物战场容器 │ ├── MapScenes/ │ │ ├── MapMain.tscn # 爬塔地图主场景 │ │ ├── MapNode.tscn # 可复用地图节点预制体 │ │ └── MapBackground.tscn # 地图背景容器 │ ├── UIScenes/ │ │ ├── Popups/ # 各类弹窗预制体(奖励、提示、确认框) │ │ ├── Tooltips/ # 悬浮提示预制体 │ │ └── OverlayUI.tscn # 全局顶层UI遮罩 │ └── CharacterScenes/ │ ├── CharacterPreview.tscn # 角色选择预览场景 │ └── CharacterBattleSprite.tscn # 战斗内角色立绘节点 ├── images/ # 全部图片美术资源 │ ├── Cards/ # 卡牌原画、卡牌边框 │ ├── Monsters/ # 怪物立绘、怪物状态贴图 │ ├── Relics/ # 遗物图标素材 │ ├── Potions/ # 药水图标 │ ├── BuffIcons/ # 力量/减益图标 │ ├── UI/ # UI面板、按钮、边框素材 │ ├── MapNodes/ # 地图节点图标 │ └── Backgrounds/ # 战斗背景、地图背景图 ├── localization/ # 本地化文本资源 │ ├── en/ # 英文原始文本 │ ├── zh/ # 中文翻译文本(逆向提取) │ └── locale_metadata.json # 语言配置元数据 ├── fonts/ # 游戏使用字体文件 │ ├── MainFont/ # 正文通用字体 │ ├── TitleFont/ # 标题粗体字体 │ └── TooltipFont/ # 提示框专用字体 ├── shaders/ # GLSL着色器文件 │ ├── CardShaders/ │ │ ├── CardGlow.shader # 卡牌高亮发光效果 │ │ ├── CardGray.shader # 卡牌禁用灰度着色器 │ │ └── CardBorder.shader # 卡牌边框渲染 │ ├── DamageShaders/ │ │ └── HitFlash.shader # 受击白色闪烁效果 │ └── GeneralShaders/ │ ├── ParticleFade.shader # 粒子淡出着色器 │ └── UIMask.shader # UI遮罩裁剪着色器 ├── audio/ # 音频资源(wav/ogg音效、音乐) │ ├── sfx/ # 全部短音效 │ └── music/ # BGM音频文件 └── mods/ # 示例Mod目录(逆向工程自带测试目录)
|
内部的 project.godot 基础应用设置([application])
-
run/main_scene: 默认启动 res://scenes/game.tscn 场景节点
-
config/icon: 默认采用图片目录的 res://images/icon_1024.png(1024x1024像素)
-
config/windows_native_icon: 默认设置原生 window 系统图标 res://images/icon.ico
-
config/use_custom_user_dir=true: 自定义用户空间目录
-
config/custom_user_dir_name: 自定义用户空间目录名称, 一般采用游戏名(推荐采用全英文)
之后就是显示设置([display])
-
window/size/viewport_width=1920: 设置视窗宽度默认为 1920
-
window/size/viewport_height=1080: 设置视窗高度默认为 1080
-
window/size/initial_position_type=3: 设置初始的视窗类型为 Center of Other Screen
-
window/stretch/mode=canvas_items: 拉伸模式设置为 canvas_items
-
window/stretch/aspect=expand: 拉伸比例设置为 expand
自动加载全局功能类([autoload])
-
SentryInit(addons/sentry/SentryInit.gd)
-
OneTimeInitialization(scenes/one_time_initialization.tscn)
- 单次全局初始化工具, 用于基础环境校验、全局参数初始化、前置服务启动
- 对应挂载
src/Core/Nodes/NOneTimeInitialization.cs 脚本
- 该脚本负责初始化挂载
存档管理(SaveManager)/Mod管理(ModManager)
-
AssetLoader(scenes/asset_loader.tscn)
- 全局资源加载管理器, 用于异步预加载、资源缓存、资源卸载管控
- 对应挂载
src/Core/Nodes/NAssetLoader.cs 脚本
- 该脚本用在后台队列会话场景加载, 采用
AssetLoadingSession 场景做会话管理
-
DevConsole(scenes/debug/dev_console.tscn)
- 开发者调试控制台, 支持指令输入、运行时变量修改、调试指令执行
- 编写特殊得系统指令功能, 用于游戏当中的调试和数值修改
-
CommandHistory(scenes/debug/command_history.tscn)
- 配套调试控制台, 用于缓存输入命令、上下键历史回溯
-
MemoryMonitor(scenes/debug/memory_monitor.tscn)
- 调试工具, 实时内存监控、资源泄漏检测、内存快照输出
-
FmodManager(addons/fmod/FmodManager.gd)
这里面和游戏相关的就是 OneTimeInitialization/AssetLoader, 其他依靠附属的开发工具
游戏默认启动 scenes/game.tscn 场景来作为游戏根场景, 之后就是系统加载流程, 内部加载大量系统工具管理器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| [根节点] Game (Control) # 挂载 src/Core/Nodes/NGame.cs ├─ FmodBankLoader # 预加载FMOD音效资源包 ├─ AudioManager # NAudioManager, 全局音频控制 ├─ ScreenShake(NScreenShake) # 屏幕震动全局管理器 ├─ HitStop(NHitStop) # 击中慢动作全局管理 ├─ InputManager # 输入、手柄控制器、热键管理 ├─ CursorManager # 鼠标光标样式切换 ├─ RootSceneContainer # [重中之重]场景容器(scene_container.tscn) │ # 所有业务场景(main_menu/map_main/combat_main)作为其子节点加载 ├─ InspectionContainer # 卡牌详情悬浮面板容器(顶层UI) ├─ RemoteCursorContainer # 多人联机远程鼠标 ├─ ReactionWheel/ReactionContainer # 互动轮盘 ├─ HoverTipsContainer # 悬浮提示文本容器 ├─ ModalContainer # 弹窗容器(商店、事件弹窗、确认框) ├─ FeedbackScreen # 反馈弹窗 ├─ GameTransitionRect(NTransition) # 场景过渡器 │ ├─ GradientTransition │ └─ SimpleTransition # 两种画面过渡动画(黑屏渐变/战斗专属过渡材质) ├─ MultiplayerTimeoutOverlay # 多人联机超时提示 ├─ WorldEnvironment # 全局渲染环境、Glow泛光设置 ├─ MuteInBackgroundHandler # 窗口失去焦点静音 └─ BackgroundModeHandler # 后台运行策略
|
也就是说, 整个游戏只有 Game 作为根节点, 其他场景都是作为子节点附加在之后运行
具体的信息可以参考如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| │ ├── 资源加载层 (src/Core/Assets/) ────────────────── │ ├── AssetCache.cs → 资源缓存池 (避免重复加载) │ ├── AssetSets.cs → 资源集合定义 (按类型/场景分组加载) │ ├── AssetLoadingSession.cs→ 异步资源加载会话 (236行, 完整加载流程) │ ├── PreloadManager.cs → 预加载管理器 │ ├── AtlasManager.cs → 图集管理器 (SpriteAtlas 的统一接口) │ └── TpSheet*.cs → TexturePacker 精灵表数据解析 │ ├── 音频层 (src/Core/Audio/) ─────────────────────── │ ├── FmodSfx.cs → FMOD 音效播放封装 (可替换为其他音频方案) │ ├── DamageSfxType.cs → 音效类型枚举模式 (可学习其分类设计) │ └── Debug/NDebugAudioManager.cs → 调试用音频管理 │ ├── 本地化层 (src/Core/Localization/) ────────────── │ ├── DynamicVars/ → 运行时文本变量替换 (如 "{damage}点伤害") │ ├── Fonts/ → 多语言字体回退链 (日/韩/俄/泰/中) │ ├── Formatters/ → SmartFormat 格式化器封装 │ └── localization/目录 → 14语言 CSV/JSON 翻译文件结构 │ ├── 存档系统 (src/Core/Saves/) ────────────────────── │ ├── Managers/ → 存档管理器 (多槽位/自动保存) │ ├── Migrations/ → (推荐学习) 存档版本迁移机制 (版本更新不丢档) │ ├── Validation/ → 存档完整性校验 │ └── Runs/ → 单次游戏运行数据序列化 │ ├── 设置系统 (src/Core/Settings/) ────────────────── │ └── 音量/画面/按键绑定 等持久化设置管理 │ ├── 平台抽象 (src/Core/Platform/) ────────────────── │ ├── Null/ → 空平台默认实现 (无Steam等环境) │ └── Steam/ → Steam SDK 集成 (成就/排行榜/Workshop) │ ├── 输入系统 (src/Core/ControllerInput/) ─────────── │ └── ControllerConfigs/ → 手柄按键映射配置 (Xbox/PS/Switch) │ ├── 开发者工具 (src/Core/DevConsole/) ────────────── │ ├── ConsoleCommands/ → 控制台命令注册/解析/执行 │ └── 配合 scenes/debug/dev_console.tscn 使用 │ ├── 日志系统 (src/Core/Logging/) ─────────────────── │ └── 结构化日志封装 (便于调试和崩溃报告) │ ├── 扩展工具 (src/Core/Extensions/) ──────────────── │ └── C# 扩展方法集 (字符串/集合/Godot类型等通用扩展) │ ├── 节点扩展 (src/Core/Nodes/GodotExtensions/) ───── │ └── Godot 框架的补充工具方法 │ ├── 对象池 (src/Core/Nodes/Pooling/) ─────────────── │ └── (推荐学习) 高性能节点对象池 (子弹/特效/卡牌等重复创建对象) │ ├── 随机数系统 (src/Core/Random/) ────────────────── │ └── 可种子的随机数生成器 (确保回放一致性) │ └── 命令模式 (src/Core/Commands/) ────────────────── └── 操作抽象 + 撤销/重做基础 (可用于任何需要Undo的系统)
|
这里我借助民间文档和 AI 解析整体解包逻辑, 分析出可以引入到个人项目的流程
实际上在细读这部分代码之后, 发现内部项目其实问题还是挺多的; 游戏项目大范围采用硬编码形式写死场景路径, 很多时候能够看到内部写实的路径
1 2
| "res://scenes/screens/*." "res://images/map/placeholder/" + base.Id.Entry.ToLowerInvariant() + "_icon"
|
这部分代码后续维护和修改是很麻烦的, 不过直接编码写入的好处就是足够简单高效, 所以这部分仁者见仁智者见智
资源加载系统
最值得移植基础模块, 用于负责游戏内部的资源管理, 首先定义资源异常对象
1 2 3 4 5 6 7 8 9
|
public class AssetLoadException : Exception { public AssetLoadException(string message) : base(message) { } public AssetLoadException(string message, Exception inner) : base(message, inner) { } }
|
对于 资源缓存(AssetCache) 也是直接采用自定义类, 参考 src/Core/Assets/AssetCache.cs, 具体关注以下信息:
-
Dictionary<string, Resource> _cache: 用于统一管理存储已加载资源
-
Get<T>(path): 用于确认缓存命中返回/未命中加载并缓存
-
Preload(paths): 批量预加载缓存资源
-
Clear(): 清空缓存, 切换场景时释放资源
具体关键代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181
| using System.Collections.Concurrent; using System.Collections.Generic; using Godot;
public class AssetCache {
private readonly ConcurrentDictionary<string, Resource> _cache = new ConcurrentDictionary<string, Resource>();
private readonly HashSet<string> _missedCacheAssets = new HashSet<string>();
private readonly HashSet<string> _failedAssets = new HashSet<string>();
private Resource GetAsset(string path) { if (_cache.TryGetValue(path, out Resource value)) { return value; }
return LoadAsset(path); }
public TS GetAsset<TS>(string path) where TS : Resource { return (TS)GetAsset(path); }
private Resource LoadAsset(string path) { if (_failedAssets.Contains(path)) { throw new AssetLoadException("Asset previously failed to load: " + path + ". The game installation may be corrupted."); }
_missedCacheAssets.Add(path); _cache[path] = ResourceLoader.Load<Resource>(path, null, ResourceLoader.CacheMode.Reuse); return _cache[path]; }
public void UnloadAssets(IEnumerable<string> assetsToUnloadSet) { foreach (string item in assetsToUnloadSet) { if (!_missedCacheAssets.Contains(item)) { RemoveAndGetResource(item)?.Dispose(); } } }
private Resource? RemoveAndGetResource(string key) { if (_cache.TryRemove(key, out Resource value)) { return value; }
return null; }
public bool ContainsKey(string s) { return _cache.ContainsKey(s); }
public void SetAsset(string path, Resource resource) { _cache[path] = resource; }
public void MarkAssetFailed(string path) { _failedAssets.Add(path); } public PackedScene GetScene(string path) { return (PackedScene)GetAsset(path); }
public Texture2D GetTexture2D(string path) { return (Texture2D)GetAsset(path); }
public Material GetMaterial(string path) { return (Material)GetAsset(path); }
public CompressedTexture2D GetCompressedTexture2D(string path) { return (CompressedTexture2D)GetAsset(path); } }
|
内部的日志系统推荐不照抄, 而是自己去 C# 社区对应的日志系统, 比如 Serilog 这类社区日志库
场景切换
游戏的场景切换也是可以借鉴学习的, 整体架构启动架构如下
1 2 3 4 5 6 7 8 9 10 11 12
| ┌──────────────────────────────────────────────────── │ scenes/game.tscn (入口场景) │ └── NGame (Control) ← 总控制器 │ ├── NSceneContainer ← 场景容器 │ │ └── [当前显示的场景] │ │ ├── NLogoAnimation (Logo动画) │ │ ├── NMainMenu (主菜单) │ │ ├── NRun (游戏内) │ │ └── ... │ ├── NTransition ← 过渡特效 │ └── [其他常驻组件] └───────────────────────────────────────────────────
|
实际上的游戏启动流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 主菜单 → 开始游戏:
await Transition.FadeOut(); // 1. 黑屏遮罩 await PreloadManager.LoadRunAssets(character); // 2. 预加载游戏资源 RootSceneContainer.SetCurrentScene(NRun.Create()); // 3. 替换场景 await RunManager.Instance.EnterAct(0); // 4. 初始化游戏逻辑 // (FadeIn 由 RunManager 内部触发)
游戏结束 → 返回主菜单:
await Transition.FadeOut(); // 1. 黑屏遮罩 await PreloadManager.LoadCommonAndMainMenuAssets(); // 2. 重新加载主菜单资源 RunManager.Instance.CleanUp(); // 3. 清理游戏状态 await LoadMainMenu(); // 4. 替换为主菜单场景
|
首先是 场景容器(NSceneContainer) 的功能说明:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
| using Godot;
public partial class NSceneContainer : Control { private Control? _currentScene;
public Control? CurrentScene { get { if (_currentScene == null) { return null; }
if (!GodotObject.IsInstanceValid(_currentScene)) { return null; }
if (_currentScene.IsQueuedForDeletion()) { return null; }
return _currentScene; } private set => _currentScene = value; }
public void SetCurrentScene(Control node) { foreach (Node child in GetChildren()) { this.RemoveChildSafely(child); child.QueueFreeSafely(); }
CurrentScene = node; if (node.GetParent() == null) { this.AddChildSafely(node); } else { node.Reparent(this); } } }
|
主要的涉及要点如下
当然直接场景切换是很生硬的, 所以也就衍生出 过渡特效(NTransition) 的节点类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| public partial class NTransition : ColorRect { public async Task FadeOut(float time = 0.8f, string transitionPath = "res://materials/transitions/fade_transition_mat.tres", CancellationToken? cancelToken = null) { } public async Task FadeIn(float time = 0.8f, string transitionPath = "res://materials/transitions/fade_transition_mat.tres", CancellationToken? cancelToken = null) { }
public async Task RoomFadeOut() { } public async Task RoomFadeIn() { } }
|
| 方法 |
效果 |
overlay 的 alpha |
画面状态 |
FadeOut() |
画面淡出(黑屏遮住) |
0 → 1 |
旧场景被遮挡, 用户看到纯黑 |
FadeIn() |
画面淡入(黑屏揭开) |
1 → 0 |
新场景显露, 黑屏完全透明 |
这里先不做复杂实现, 可以简单自己做初级黑屏淡入淡出效果, 这边提供最初级的参考功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
| using Godot; using System.Threading.Tasks;
namespace Layout.Core.Nodes;
public partial class NTransition : ColorRect { public bool InTransition { get; private set; }
public override void _Ready() { }
public async Task FadeOut(float duration = 0.8f) { Color = new Color(Color, 0.0f); InTransition = true; Visible = true; MouseFilter = MouseFilterEnum.Stop;
var tween = CreateTween().SetParallel(); tween.SetEase(Tween.EaseType.In).SetTrans(Tween.TransitionType.Quad); tween.TweenProperty(this, "color:a", 1.0f, duration); await ToSignal(tween, Tween.SignalName.Finished); }
public async Task FadeIn(float duration = 0.8f) { Color = new Color(Color, 1f); var tween = CreateTween().SetParallel(); tween.SetEase(Tween.EaseType.Out).SetTrans(Tween.TransitionType.Quad); tween.TweenProperty(this, "color:a", 0.0f, duration); await ToSignal(tween, Tween.SignalName.Finished);
Visible = false; MouseFilter = MouseFilterEnum.Ignore; InTransition = false; }
public async Task Transition(Task sceneTask, float fadeOutDuration = 0.8f, float fadeInDuration = 0.8f) { await FadeOut(fadeOutDuration); await sceneTask; await FadeIn(fadeInDuration); } }
|
这样就能做个简单过渡效果类, 之后在主场景设定启动功能即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112
| using System; using System.Threading.Tasks; using Godot; using Environment = System.Environment;
namespace Layout.Core.Nodes;
public partial class NGame : Node { public static NGame? Instance { get; private set; }
public NTransition Transition { get; private set; }
[Export] public Button SceneButton { get; set; }
#region 是否运行主线程
private static int? _mainThreadId;
public static bool MainThreadId { get { if (_mainThreadId.HasValue) return _mainThreadId == Environment.CurrentManagedThreadId; _mainThreadId = Environment.CurrentManagedThreadId; return true; } }
#endregion
public override void _EnterTree() { if (Instance != null) { GD.PrintErr("You are already running this game!"); QueueFree(); return; }
Instance = this;
Transition = GetNode<NTransition>("%GameTransitionRect"); Transition.Color = Colors.Black; SceneButton.Pressed += OnSceneButtonDown;
try { _ = LoadMainScene(); } catch (Exception ex) { if (ex is not TaskCanceledException) { GD.PrintErr(ex.ToString()); }
throw; } }
private async Task LoadMainScene() { await Transition.FadeIn(0.8f); }
private async void OnSceneButtonDown() { await Transition.FadeOut(0.8f); await Transition.FadeIn(0.8f); } }
|
这里挂载的节点结构如下, 需要将该节点设定为全局唯一节点采用被直接检索到

可以说这份游戏代码项目有很多值得借鉴的地方, 碍于篇幅过长只能稍微介绍下部分比较实用的点, 后续会单独针对某些模块实现来说明