网站开发人员上级主管,免费优化关键词,和动物做的网站吗,目前最流行网站开发软件目录
前言
全局协程还是实例协程#xff1f;
存档#xff01;
全局管理类#xff1f;
UI框架#xff1f;
Godot中的异步#xff08;多线程#xff09;加载
Godot中的ScriptableObject
游戏流程思考
结语 前言 这是一篇杂谈#xff0c;主要内容是对我…目录
前言
全局协程还是实例协程
存档
全局管理类
UI框架
Godot中的异步多线程加载
Godot中的ScriptableObject
游戏流程思考
结语 前言 这是一篇杂谈主要内容是对我近期在做的事做一些简单的小总结和探讨包括整理Godot开发工具和思考Godot开发核心。 因为太久没写东西了于是随性地写一点吧有啥说啥。
全局协程还是实例协程 不得不说在“深入”了一段时间后发现协程这个东西对于游戏而言非常重要。因为很多东西是需要在多帧完成的而非一帧之内完成的所以有必要优化一下这方面的体验为此我特意强化了一下常用的协程系统 等等如果看不懂很正常因为我压根没打算细说只是为了表示个协程系统的大概。对协程感兴趣可以先看看这里
C# 游戏引擎中的协程_c# 协程-CSDN博客https://blog.csdn.net/m0_73087695/article/details/142462298?spm1001.2014.3001.5501
我们知道Unity里面的协程是以MonoBehaviour为单位的也就是一个MonoBehaviour负责管理它自己的协程。因为我比较懒就索性搞了了全局的协程“启动器”以此来满足快速启动某个协程的需求。 以目前我对协程的理解我只能肤浅的把它们分为两类分别对应Godot的两种帧处理方法。
增添改查倒不用多说了这个类会作为一个单例节点在“Autoload”的加持下加入树中。以此才能处理协程。 其实以这个思路在每个节点上都装载一个“协程管理器”倒是不难不过我对这样做的必要性存疑而且我以前写Unity的时候因为每个实例一堆协程而绕晕过于是就没有这么干了懒。 暂且先将一堆协程放在一起吧当然想要属于节点自己的协程可以直接new出来。
using System;
using System.Collections;
using System.Collections.Generic;namespace GoDogKit
{/// summary/// In order to simplify coroutine management, /// this class provides a global singleton that can be used to launch and manage coroutines./// It will be autoloaded by GodogKit./// /summarypublic partial class GlobalCoroutineLauncher : SingletonGlobalCoroutineLauncher{private GlobalCoroutineLauncher() { }private readonly ListCoroutine m_ProcessCoroutines [];private readonly ListCoroutine m_PhysicsProcessCoroutines [];private readonly DictionaryIEnumerator, ListCoroutine m_Coroutine2List [];private readonly QueueAction m_DeferredRemoveQueue [];public override void _Process(double delta){ProcessCoroutines(m_ProcessCoroutines, delta);}public override void _PhysicsProcess(double delta){ProcessCoroutines(m_PhysicsProcessCoroutines, delta);}public static void AddCoroutine(Coroutine coroutine, CoroutineProcessMode mode){switch (mode){case CoroutineProcessMode.Idle:Instance.m_ProcessCoroutines.Add(coroutine);Instance.m_Coroutine2List.Add(coroutine.GetEnumerator(), Instance.m_ProcessCoroutines);break;case CoroutineProcessMode.Physics:Instance.m_PhysicsProcessCoroutines.Add(coroutine);Instance.m_Coroutine2List.Add(coroutine.GetEnumerator(), Instance.m_PhysicsProcessCoroutines);break;}}// It batter to use IEnumerator to identify the coroutine instead of Coroutine itself.public static void RemoveCoroutine(IEnumerator enumerator){if (!Instance.m_Coroutine2List.TryGetValue(enumerator, out var coroutines)) return;int? index null;for (int i coroutines.Count - 1; i 0; i--){if (coroutines[i].GetEnumerator() enumerator){index i;break;}}if (index is not null){Instance.m_DeferredRemoveQueue.Enqueue(() coroutines.RemoveAt(index.Value));}}private static void ProcessCoroutines(ListCoroutine coroutines, double delta){foreach (var coroutine in coroutines){coroutine.Process(delta);}// Remove action should not be called while procssing.// So we need to defer it until the end of the frame.ProcessDeferredRemoves();}private static void ProcessDeferredRemoves(){if (!Instance.m_DeferredRemoveQueue.TryDequeue(out var action)) return;action();}/// summary/// Do not use if unneccessary./// /summarypublic static void Clean(){Instance.m_ProcessCoroutines.Clear();Instance.m_PhysicsProcessCoroutines.Clear();Instance.m_Coroutine2List.Clear();Instance.m_DeferredRemoveQueue.Clear();}/// summary/// Get the current number of coroutines running globally, both in Idle and Physics process modes./// /summary/// returns The number of coroutines running. /returnspublic static int GetCurrentCoroutineCount() Instance.m_ProcessCoroutines.Count Instance.m_PhysicsProcessCoroutines.Count;}
} 至于怎么快速启动 那必然是用到拓展方法。值得注意的是因为以C#枚举器进化而来的“协程”本质上是IEnumerator所以用来辨别协程的“ID”也应当是IEnumerator。就像这里的删除停止协程执行传递的是IEnumerator而非我们自己封装的协程类。 话说回来拓展方法确实非常的好用以前很少关注这个东西觉得可有可无后来发现有了拓展方法就可以写得很“糖”氏很多全局类的功能可以直接由某个实例执行就不用写很长的名字访问对应的方法。再者还可以加以抽象针对接口制作拓展方法实现某些框架等等。
#region Coroutinepublic static void StartCoroutine(this Node node, Coroutine coroutine, CoroutineProcessMode mode CoroutineProcessMode.Physics){coroutine.Start();GlobalCoroutineLauncher.AddCoroutine(coroutine, mode);}public static void StartCoroutine(this Node node, IEnumerator enumerator, CoroutineProcessMode mode CoroutineProcessMode.Physics){StartCoroutine(node, new Coroutine(enumerator), mode);}public static void StartCoroutine(this Node node, IEnumerable enumerable, CoroutineProcessMode mode CoroutineProcessMode.Physics){StartCoroutine(node, enumerable.GetEnumerator(), mode);}public static void StopCoroutine(this Node node, IEnumerator enumerator){GlobalCoroutineLauncher.RemoveCoroutine(enumerator);}public static void StopCoroutine(this Node node, Coroutine coroutine){StopCoroutine(node, coroutine.GetEnumerator());}public static void StopCoroutine(this Node node, IEnumerable enumerable){StopCoroutine(node, enumerable.GetEnumerator());}#endregion
存档 老早就应该写了但是太懒了总不能一直一直用别人的吧。Godot内置了很多文件操作API但是我还是选择了用C#库的因为普适性万一以后又跑回Unity了Copy过来还可以用Doge。 好了因为代码又臭又长了其实也不用看。简单来说一开始我试着把所谓的“存档”抽象成一个类只针对这个类进行读写以及序列化后面想了想觉得如果这样的话每次new新的“存档”又得填一边路径和序列化方式干脆搞个全局类“存档系统”每次new存档时候为“存档”自动赋初值。 很好然后我还需要很多种可用的序列化和加密方法来保证我的游戏存档是安全可靠的我应该写在哪呢难道写在每个单独的存档类里嘛不对每种序列化方法对“存档”的操作方式是不同的所以要把“存档”也细分不然不能支持多种序列化或加密方式。 可是这样我的全局类又怎么知道我想要new一个什么样的“存档”类呢在很多时候我们往往需要对不同的“存档”这里代指文本文件使用不同的处理方式比如游戏数据我们需要加密但是游戏DEBUG日志我们就不需要。那就干脆把它也抽象了吧搞一个“子存档系统”由不同的子存档系统负责管理不同需求的“存档”。 同时为了避免混乱每个“存档”都保留对管理它的子系统的引用如果一个存档没有子系统引用说明它是“野存档”。以此来约束不同种类的“存档”只能由不同种类的“子系统”创建其实就是“工厂模式”或者“抽象工厂模式”。而且在创建存档时怕自己写昏头了我不得不再对子系统抽象将创建方法抽象到一个新的泛型抽象类并借此对创建方法赋予再一级的约束。以防用某个类型的子系统创建了不属于它的类型的存档。 最终才拉出了下面这坨屎山。 有一个非常有意思蠢的点在我想给“存档”类写拓展方法时我发现底层的序列化得到的对象一直传不上来当然了这是因为引用类型作参数时还是以值的方式传递自身的引用所以序列化生成的那个对象的引用一直“迷失”在了底层的调用中我不想给存档对象写深拷贝于是尝试用ref解决结果拓展方法不能给类用ref于是果断放弃为存档类拓展方法代码中的那两个[Obsolete]就是这么来的。 后面妥协了把存档读取和加载交由子系统完成不能爽写了。 还有就是C#原生库对Json序列化的支持感觉确实不太好要支持AOT的话还得写个什么JsonSerializerContext我这里为了AOT完备不得以加之到对应子系统的构造函数中。也许XML可能会好点但是目前只写了Json一种序列化方法因为懒。
using System;
using System.IO;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Godot;namespace GoDogKit
{#region ISaveable/// summary/// Fundemental interface for all saveable objects./// Contains basical information for saving and loading, such as file name, directory, /// and the save subsystem which own this./// /summarypublic interface ISaveable{/// summary/// The file name without extension on save./// /summary public string FileName { get; set; }/// summary/// The file name extension on save./// /summary public string FileNameExtension { get; set; }/// summary/// The directory where the file is saved./// /summarypublic DirectoryInfo Directory { get; set; }/// summary/// The save subsystem which own this./// /summarypublic SaveSubsystem SaveSubsystem { get; set; }public virtual void Clone(ISaveable saveable){FileName saveable.FileName;FileNameExtension saveable.FileNameExtension;Directory saveable.Directory;SaveSubsystem saveable.SaveSubsystem;}}public class JsonSaveable : ISaveable{[JsonIgnore] public string FileName { get; set; }[JsonIgnore] public string FileNameExtension { get; set; }[JsonIgnore] public DirectoryInfo Directory { get; set; }[JsonIgnore] public SaveSubsystem SaveSubsystem { get; set; }// /// summary// /// The JsonSerializerContext used to serialize and deserialize this object.// /// /summary// [JsonIgnore] public JsonSerializerContext SerializerContext { get; set; }}#endregion#region Systempublic static class SaveSystem{public static DirectoryInfo DefaultSaveDirectory { get; set; }public static string DefaultSaveFileName { get; set; } sg;public static string DefaultSaveFileExtension { get; set; } .data;public static SaveEncryption DefaultEncryption { get; set; } SaveEncryption.Default;static SaveSystem(){if (OS.HasFeature(editor)){// If current save action happens in editor, // append with _Editor in project folder root.DefaultSaveDirectory new DirectoryInfo(Save_Editor);}else{// Else, use the Save folder to store the save file,// at the same path with the game executable in default.DefaultSaveDirectory new DirectoryInfo(Save);}if (!DefaultSaveDirectory.Exists){DefaultSaveDirectory.Create();}}public static string Encrypt(string data, SaveEncryption encryption){return encryption.Encrypt(data);}public static string Decrypt(string data, SaveEncryption encryption){return encryption.Decrypt(data);}public static bool Exists(ISaveable saveable){return File.Exists(GetFullPath(saveable));}public static string GetFullPath(ISaveable saveable){return Path.Combine(saveable.Directory.FullName, saveable.FileName saveable.FileNameExtension);}public static void Delete(ISaveable saveable){if (Exists(saveable)){File.Delete(GetFullPath(saveable));}}/// summary/// Checks if there are any files in the systems save directory./// It will count the number of files with the same extension as the systems /// by default./// /summary/// param namesystem The save subsystem to check. /param/// param namesaveNumber The number of files found. /param/// param nameextensionCheck Whether to check the file extension or not. /param/// returns/returnspublic static bool HasFiles(SaveSubsystem system, out int saveNumber, bool extensionCheck true){var fileInfos system.SaveDirectory.GetFiles();saveNumber 0;if (fileInfos.Length 0){return false;}if (extensionCheck){foreach (var fileInfo in fileInfos){if (fileInfo.Extension system.SaveFileExtension){saveNumber;}}if (saveNumber 0) return false;}else{saveNumber fileInfos.Length;}return true;}}/// summary/// Base abstract class for all save subsystems./// /summarypublic abstract class SaveSubsystem{public DirectoryInfo SaveDirectory { get; set; } SaveSystem.DefaultSaveDirectory;public string SaveFileName { get; set; } SaveSystem.DefaultSaveFileName;public string SaveFileExtension { get; set; } SaveSystem.DefaultSaveFileExtension;public SaveEncryption Encryption { get; set; } SaveSystem.DefaultEncryption;public abstract string Serialize(ISaveable saveable);public abstract ISaveable Deserialize(string data, ISaveable saveable);public virtual void Save(ISaveable saveable){string data Serialize(saveable);string encryptedData SaveSystem.Encrypt(data, Encryption);File.WriteAllText(SaveSystem.GetFullPath(saveable), encryptedData);}public virtual ISaveable Load(ISaveable saveable){if (!SaveSystem.Exists(saveable)) throw new FileNotFoundException(Save file not found!);string data File.ReadAllText(SaveSystem.GetFullPath(saveable));string decryptedData SaveSystem.Decrypt(data, Encryption);var newSaveable Deserialize(decryptedData, saveable);newSaveable.Clone(saveable);return newSaveable;}public virtual Task SaveAsync(ISaveable saveable){string data Serialize(saveable);string encryptedData SaveSystem.Encrypt(data, Encryption);return File.WriteAllTextAsync(SaveSystem.GetFullPath(saveable), encryptedData);}public virtual TaskISaveable LoadAsync(ISaveable saveable){if (!SaveSystem.Exists(saveable)) throw new FileNotFoundException(Save file not found!);return File.ReadAllTextAsync(SaveSystem.GetFullPath(saveable)).ContinueWith(task {string data task.Result;string decryptedData SaveSystem.Decrypt(data, Encryption);var newSaveable Deserialize(decryptedData, saveable);newSaveable.Clone(saveable);return newSaveable;});}}/// summary/// Abstract class for all functional save subsystems./// Restricts the type of ISaveable to a specific type, /// providing a factory method for creating ISaveables./// /summary/// typeparam nameT/typeparampublic abstract class SaveSubsystemT : SaveSubsystem where T : ISaveable, new(){public virtual S CreateS() where S : T, new(){var ISaveable new S(){FileName SaveFileName,FileNameExtension SaveFileExtension,Directory SaveDirectory,SaveSubsystem this};return ISaveable;}}/// summary/// /// A Sub save system that uses the JsonSerializer in dotnet core./// Notice that a JsonSerializerContext is required to be passed in the constructor,/// for AOT completeness./// para So you need to code like this as an example: /para/// sample/// /// para [JsonSerializable(typeof(SaveData))] /para/// /// para public partial class DataContext : JsonSerializerContext { } /para/// /// para public class SaveData : JsonISaveable /para/// para { /para/// para public int Health { get; set; } /para/// para } /para/// /// /sample/// /summarypublic class JsonSaveSubsystem(JsonSerializerContext serializerContext) : SaveSubsystemJsonSaveable{public readonly JsonSerializerContext SerializerContext serializerContext;public override string Serialize(ISaveable saveable) JsonSerializer.Serialize(saveable, saveable.GetType(), SerializerContext);public override ISaveable Deserialize(string data, ISaveable saveable) JsonSerializer.Deserialize(data, saveable.GetType(), SerializerContext) as ISaveable;}#endregion#region Extension Methods/// summary/// All functions used to extend the SaveSystem class. Fully optional, but recommended to use./// /summarypublic static class SaveSystemExtensions{[Obsolete(Use Subsystem.Save() instead.)]public static void Save(this ISaveable saveable){saveable.SaveSubsystem.Save(saveable);}/// summary/// Unfortuantely, Extension Methods do not support ref classes, so we need to recevive the return value./// /summary [Obsolete(Use Subsystem.Load() instead.)]public static T LoadT(this T saveable) where T : class, ISaveable{return saveable.SaveSubsystem.Load(saveable) as T;}/// summary/// Save a saveable into local file system depends on its own properties./// /summarypublic static void SaveT(this SaveSubsystem subsystem, T saveable) where T : class, ISaveable{subsystem.Save(saveable);}/// summary/// Load a saveable from local file system depends on its own properties./// This an alternative way to load a saveable object, remember to use a ref parameter./// /summarypublic static void LoadT(this SaveSubsystem subsystem, ref T saveable) where T : class, ISaveable{saveable subsystem.Load(saveable) as T;}public static bool Exists(this ISaveable saveable){return SaveSystem.Exists(saveable);}public static string GetFullPath(this ISaveable saveable){return SaveSystem.GetFullPath(saveable);}public static void Delete(this ISaveable saveable){SaveSystem.Delete(saveable);}public static bool HasFiles(this SaveSubsystem system, out int saveNumber, bool extensionCheck true){return SaveSystem.HasFiles(system, out saveNumber, extensionCheck);}}#endregion#region Encryptionpublic abstract class SaveEncryption{public abstract string Encrypt(string data);public abstract string Decrypt(string data);public static NoneEncryption Default { get; } new NoneEncryption();}public class NoneEncryption : SaveEncryption{public override string Encrypt(string data) data;public override string Decrypt(string data) data;}/// summary/// Encryption method in negation./// /summarypublic class NegationEncryption : SaveEncryption{public override string Encrypt(string data){byte[] bytes Encoding.Unicode.GetBytes(data);for (int i 0; i bytes.Length; i){bytes[i] (byte)~bytes[i];}return Encoding.Unicode.GetString(bytes);}public override string Decrypt(string data) Encrypt(data);}#endregion
}
全局管理类 在以前开发Unity的时候总会写一些什么全局管理类。一开始接触Godot的时候我尝试遵循Godot的开发理念即不用框架自然地思考游戏流程但最后还是忍不住写起了全局管理类。其实这些全局类仅仅只是为了简化开发流程罢了。 比如一个全局的对象池通过对文本场景文件.tscn的注册来自动生成对应的对象池并对它们进行拓展和管理。 可以看到很多情况下像这样的全局类的方法还是主要以封装其被管理对象自己的方法为主。也就是意味着我们只是写得更爽了而已把应当在开发时创建的对象池延时到了游戏运行时创建。 但是这样的方式有着更多的灵活性比如可以随时创建注册和销毁注销新的节点对于内存管理而言会比较友好我们在每个“关卡”都可以灵活地创建需要用到的节点。 再加之以拓展方法我们就可以直接针对被管理对象进行操作比如这里的PackedScene通过简单地为其拓展依赖于管理类的方法就能方便地对它自身进行管理。 看似复杂其实就是做了这样类似的事我们创建一个对象池节点把某个PackedScene赋值给对象池在其他代码中取得该对象池的引用并使用它。上面三个事在一个我所谓的“全局管理类”下三合一现在我们只需要对PackedScene本身进行引用保留然后通过拓展方法即可实现上述过程。 这当然是有好有坏的优点就是上述的灵活和便捷缺点就是不能较大程度地操作被管理对象所以我理所应当地要保留一个与原始被管理对象的接口如代码中的GetPool方法这样一来就能淡化缺点。所以就像我一开始说的那样这些有的没的管理类只是为了写得爽开发得爽而不能让你写得好开发得好。 也许是我误解了Godot的开发理念也许它的意思是“不要过于重视框架”从而让我们回到游戏开发本身而非游戏开发框架本身 于是乎现在我对“框架”的观念就是能用就行够用就行。同时在每一次开发经历中对框架进行积累和迭代。
using System.Collections.Generic;
using Godot;namespace GoDogKit
{/// summary/// A Global Manager for Object Pools, Maintains links between PackedScenes and their corresponding ObjectPools./// Provides methods to register, unregister, get and release objects from object pools./// /summarypublic partial class GlobalObjectPool : SingletonGlobalObjectPool{private readonly DictionaryPackedScene, ObjectPool ObjectPools [];/// summary/// Registers a PackedScene to the GlobalObjectPool./// /summary/// param namescene The PackedScene to register. /param/// param namepoolParent The parent node of the ObjectPool. /param/// param namepoolInitialSize The initial size of the ObjectPool. /parampublic static void Register(PackedScene scene, Node poolParent null, int poolInitialSize 10){if (Instance.ObjectPools.ContainsKey(scene)){GD.Print(scene.ResourceName already registered to GlobalObjectPool.);return;}ObjectPool pool new(){Scene scene,Parent poolParent,InitialSize poolInitialSize};Instance.AddChild(pool);Instance.ObjectPools.Add(scene, pool);}/// summary/// Unregisters a PackedScene from the GlobalObjectPool./// /summary/// param namescene The PackedScene to unregister. /parampublic static void Unregister(PackedScene scene){if (!Instance.ObjectPools.TryGetValue(scene, out ObjectPool pool)){GD.Print(scene.ResourceName not registered to GlobalObjectPool.);return;}pool.Destroy();Instance.ObjectPools.Remove(scene);}//Just for simplify coding. Ensure the pool has always been registered.private static ObjectPool ForceGetPool(PackedScene scene){if (!Instance.ObjectPools.TryGetValue(scene, out ObjectPool pool)){Register(scene);pool Instance.ObjectPools[scene];}return pool;}/// summary/// Get a node from the corresponding ObjectPool of the given PackedScene./// /summary/// param namescene The PackedScene to get the node from. /param/// returns The node from the corresponding ObjectPool. /returnspublic static Node Get(PackedScene scene){return ForceGetPool(scene).Get();}/// summary/// Get a node from the corresponding ObjectPool of the given PackedScene as a specific type./// /summary/// param namescene The PackedScene to get the node from. /param/// typeparam nameT The type to cast the node to. /typeparam/// returns The node from the corresponding ObjectPool. /returnspublic static T GetT(PackedScene scene) where T : Node{return Get(scene) as T;}/// summary/// Releases a node back to the corresponding ObjectPool of the given PackedScene./// /summary/// param namescene The PackedScene to release the node to. /param/// param namenode The node to release. /parampublic static void Release(PackedScene scene, Node node){ForceGetPool(scene).Release(node);}/// summary/// Unregisters all the PackedScenes from the GlobalObjectPool./// /summarypublic static void UnregisterAll(){foreach (var pool in Instance.ObjectPools.Values){pool.Destroy();}Instance.ObjectPools.Clear();}/// summary/// Get the ObjectPool of the given PackedScene./// If the PackedScene is not registered, it will be registered./// /summary/// param namescene The PackedScene to get the ObjectPool of. /param/// returns The ObjectPool of the given PackedScene. /returnspublic static ObjectPool GetPool(PackedScene scene){return ForceGetPool(scene);}}
} 除了对对象池或者说PackScene进行管理之外我还“东施效颦”地为音频流作了个管理类即AudioStream这一资源类型不过对于音频而言这一管理类只能管理非空间型音频Non-spatial也就是说那些与位置相关的2D或3D音频还得另外设计不过也够用了。 说到节点位置这里还是要提醒一下Node是没有位置信息xyz坐标的Node2D和Node3D有。考虑一下情况选哟把一堆节点塞到一个父节点里以方便管理但是又希望能保持父子节点之间的相对位置那么一定不能选择Node节点就是节点节点因为它没有位置信息所以它和字节点之间的相对位置是不确定的我猜它的子节点的位置可能就直接是全局位置了。 最后我还是想说你或许已经注意到了我这里所谓的“管理类”都有一个共性即是通过对某种资源绑定对应的某个节点以此简化灵活化该资源的使用流程。比如PackScene是一种Godot资源全局对象池建立该资源与对象池节点的对应关系直接管理对象池节点以此简化了该资源的使用过程。 我个人认为这是一种非常好的游戏框架思路即简化游戏资源资产的使用流程而非复杂化。虽然我同样感觉这种思路仅仅适用于小型游戏开发但是我能不能剑走偏锋将其做到极致呢
UI框架 我是真心觉得Godot不需要UI框架因为我思来想去也不知道写个框架出来能管到什么东西因为节点信号已经能很好地实现UI设计了。为此我只是简单地为UI写了个小脚本刻意写一些简单的方法留给信号使用所以在Godot里面做UI基本上和连连看差不多。 比如下面这个临时赶出来进行演示的加载场景类 这是一个用来充当“加载界面UI的节点”主要任务是异步加载多线程加载指定路径的场景后根据指定行为等待跳转Skip。就是我们常见的加载画面有个进度条表示进度有时可能会有“按下任意键继续”就这么个东西。 先不管别的有的没的直接看到自定义的ProgressChanged信号注意到该信号有一个double类型的参数借此我们就可以在制作加载画面UI时直接以信号连接的方式传递加载进度。
比如以该信号连接ProgressBar节点这是个Godot内置的节点的set_value方法并调整合适的进度步数和值就可以很轻松的实现一个简易的加载画面。 在加之以输入检测功能比如代码中我用一个InputEvent类型的Array来表示可以Skip的输入类型这样就可以在Inspector轻松赋值同时只要进行相应的类型检查就可以得到那种检测某种类型的输入才会跳转画面的效果。 这样看来只要提供一些范式的功能方法。便可以通过信号快速地构建高效的UI甚至整个游戏这确实是Godot的一大优势相对于Unity来说。
using Godot;
using Godot.Collections;namespace GoDogKit
{public partial class CutScene : Control{[Export] public string Path { get; set; }[Export] public bool AutoSkip { get; set; }[Export] public bool InputSkip { get; set; }[Export] public ArrayInputEvent SkipInputs { get; set; }[Signal] public delegate void LoadedEventHandler();[Signal] public delegate void ProgressChangedEventHandler(double progress);private LoadTaskPackedScene m_LoadTask;public override void _Ready(){m_LoadTask RuntimeLoader.LoadPackedScene(Path);if (AutoSkip){Loaded Skip;}}public override void _Process(double delta){// GD.Print(progress: m_LoadTask.Progress status: m_LoadTask.Status);EmitSignal(SignalName.ProgressChanged, m_LoadTask.Progress);if (m_LoadTask.Status ResourceLoader.ThreadLoadStatus.Loaded)EmitSignal(SignalName.Loaded);}public override void _Input(InputEvent event){if (InputSkip m_LoadTask.Status ResourceLoader.ThreadLoadStatus.Loaded){foreach (InputEvent skipEvent in SkipInputs){if (event.GetType() skipEvent.GetType()) Skip();}}}public void Skip(){GetTree().ChangeSceneToPacked(m_LoadTask.Result);}}
}
Godot中的异步多线程加载 以防你对上述代码中的RuntimeLoader感兴趣这个静态类是我封装起来专门用于异步加载资源的。在Unity中异步加载的操作比较丰富而且更加完善但到了Godot中确实是不如Unity这般丰富。 最简单的获取异步任务的需求在Godot中都会以比较繁琐的形式出现索性就把他们全部封装起来思路还是相当简单的只要弄明白那三个内置的多线程加载函数都有什么用就很容易理解了请自行查阅手册。 值得一提的是那个GetStatus虽然没有用C#中的ref之类的关键字但是还是利用底层C的优势把值传回了实参。 还有就是最后的LoadT泛型方法必须new的是泛型的LoadTask而非普通的。否侧会报一个空引用的错误我没有深究原因不过大概跟强制转换有关。 如此一来就可以畅快地在Godot异步加载资源了。
using Godot;
using Godot.Collections;namespace GoDogKit
{public class LoadTask(string targetPath){public string TargetPath { get; } targetPath;/// summary/// Represents the progress of the load operation, ranges from 0 to 1./// /summary public double Progress{get{Update();return (double)m_Progress[0];}}protected Array m_Progress [];public ResourceLoader.ThreadLoadStatus Status{get{Update();return m_Status;}}private ResourceLoader.ThreadLoadStatus m_Status;public Resource Result{get{return ResourceLoader.LoadThreadedGet(TargetPath);}}public LoadTask Load(string typeHint , bool useSubThreads false, ResourceLoader.CacheMode cacheMode ResourceLoader.CacheMode.Reuse){ResourceLoader.LoadThreadedRequest(TargetPath, typeHint, useSubThreads, cacheMode);return this;}protected void Update(){m_Status ResourceLoader.LoadThreadedGetStatus(TargetPath, m_Progress);}}public class LoadTaskT(string targetPath) : LoadTask(targetPath) where T : Resource{public new T Result{get{return ResourceLoader.LoadThreadedGet(TargetPath) as T;}}}/// summary/// Provides some helper methods for loading resources in runtime./// Most of them serve as async wrappers of the ResourceLoader class./// /summarypublic static class RuntimeLoader{/// summary/// Loads a resource from the given path asynchronously and returns a LoadTask object/// that can be used to track the progress and result of the load operation./// /summary public static LoadTask Load(string path, string typeHint , bool useSubThreads false, ResourceLoader.CacheMode cacheMode ResourceLoader.CacheMode.Reuse){return new LoadTask(path).Load(typeHint, useSubThreads, cacheMode);}/// summary/// Loads a resource from the given path asynchronously and returns a LoadTask object/// that can be used to track the progress and result of the load operation./// /summarypublic static LoadTaskT LoadT(string path, string typeHint , bool useSubThreads false, ResourceLoader.CacheMode cacheMode ResourceLoader.CacheMode.Reuse) where T : Resource{return new LoadTaskT(path).Load(typeHint, useSubThreads, cacheMode) as LoadTaskT;}}}
Godot中的ScriptableObject 我忘记我在之前的文章中有没有记录过了反正现在先记录一下吧。 作为一个Unity逃兵写不到ScriptableObject以下简称SO是无法进行游戏开发的一开始我以为Godot是没有这种东西的在加上Godot的Inspector序列化支持得不是很好TMD根本没有想要在Inspector中设定自己的数据类型简直不要太绝望。 好在我发现了GlobalClass的存在在Godot C#中作为一个属性。可以将指定的类暴露给编辑器这样一来如果该类继承自Resource之类的可以在编辑器中保存的文件类型就可以实现近似于SO的功能甚至超越。 [GlobalClass]public partial class ItemDropInfo : Resource{[Export] public int ID { get; set; }[Export] public int Amount { get; set; }[Export] public float Probability { get; set; }} 只要像这样我们就可以在编辑器中创建保存和修改该类型。
游戏流程思考 其实在复盘的相当长的时间内 我很希望能把游戏流程抽象成可以被管理的对象但是鉴于那难度之大和不同游戏类型的流程差异太多不利于框架复用。于是短时间内放弃了这一想法。 转而研究了很多这种小东西也算是受益匪浅。
结语 其实开发了这么久对游戏引擎的共性之间多少有些了解了做得越久越发明白“引擎不重要”是什么意思也越来越觉得清晰的设计思路比框架更重要。 本来还有很多话但是到此为止吧我的经验已经不够用了也许下一次“杂谈”能更加侃侃而谈吧。