Godot的开发框架应当是什么样子的?

目录

前言

全局协程还是实例协程?

存档!

全局管理类?

UI框架? 

Godot中的异步(多线程)加载

Godot中的ScriptableObject         

游戏流程思考 

结语


前言

        这是一篇杂谈,主要内容是对我近期在做的事做一些简单的小总结和探讨,包括整理Godot开发工具和思考Godot开发核心。

        因为太久没写东西了,于是随性地写一点吧,有啥说啥。

全局协程还是实例协程?

        不得不说,在“深入”了一段时间后,发现协程这个东西对于游戏而言非常重要。因为很多东西是需要在多帧完成的而非一帧之内完成的,所以有必要优化一下这方面的体验,为此我特意强化了一下常用的协程系统:

        等等,如果看不懂很正常,因为我压根没打算细说,只是为了表示个协程系统的大概。对协程感兴趣可以先看看这里:

C# 游戏引擎中的协程_c# 协程-CSDN博客icon-default.png?t=O83Ahttps://blog.csdn.net/m0_73087695/article/details/142462298?spm=1001.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./// </summary>public partial class GlobalCoroutineLauncher : Singleton<GlobalCoroutineLauncher>{private GlobalCoroutineLauncher() { }private readonly List<Coroutine> m_ProcessCoroutines = [];private readonly List<Coroutine> m_PhysicsProcessCoroutines = [];private readonly Dictionary<IEnumerator, List<Coroutine>> m_Coroutine2List = [];private readonly Queue<Action> 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(List<Coroutine> 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./// </summary>public 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. </returns>public 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./// </summary>public 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./// </summary>public DirectoryInfo Directory { get; set; }/// <summary>/// The save subsystem which own this./// </summary>public 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 system's save directory./// It will count the number of files with the same extension as the system's /// by default./// </summary>/// <param name="system"> The save subsystem to check. </param>/// <param name="saveNumber"> The number of files found. </param>/// <param name="extensionCheck"> Whether to check the file extension or not. </param>/// <returns></returns>public 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./// </summary>public 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 Task<ISaveable> 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 name="T"></typeparam>public abstract class SaveSubsystem<T> : SaveSubsystem where T : ISaveable, new(){public virtual S Create<S>() 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>/// </summary>public class JsonSaveSubsystem(JsonSerializerContext serializerContext) : SaveSubsystem<JsonSaveable>{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./// </summary>public 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 Load<T>(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./// </summary>public static void Save<T>(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./// </summary>public static void Load<T>(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./// </summary>public 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./// </summary>public partial class GlobalObjectPool : Singleton<GlobalObjectPool>{private readonly Dictionary<PackedScene, ObjectPool> ObjectPools = [];/// <summary>/// Registers a PackedScene to the GlobalObjectPool./// </summary>/// <param name="scene"> The PackedScene to register. </param>/// <param name="poolParent"> The parent node of the ObjectPool. </param>/// <param name="poolInitialSize"> The initial size of the ObjectPool. </param>public 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 name="scene"> The PackedScene to unregister. </param>public 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 name="scene"> The PackedScene to get the node from. </param>/// <returns> The node from the corresponding ObjectPool. </returns>public 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 name="scene"> The PackedScene to get the node from. </param>/// <typeparam name="T"> The type to cast the node to. </typeparam>/// <returns> The node from the corresponding ObjectPool. </returns>public static T Get<T>(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 name="scene"> The PackedScene to release the node to. </param>/// <param name="node"> The node to release. </param>public static void Release(PackedScene scene, Node node){ForceGetPool(scene).Release(node);}/// <summary>/// Unregisters all the PackedScenes from the GlobalObjectPool./// </summary>public 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 name="scene"> The PackedScene to get the ObjectPool of. </param>/// <returns> The ObjectPool of the given PackedScene. </returns>public 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 Array<InputEvent> SkipInputs { get; set; }[Signal] public delegate void LoadedEventHandler();[Signal] public delegate void ProgressChangedEventHandler(double progress);private LoadTask<PackedScene> m_LoadTask;public override void _Ready(){m_LoadTask = RuntimeLoader.Load<PackedScene>(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++的优势把值传回了实参。

        还有就是最后的Load<T>泛型方法必须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 LoadTask<T>(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./// </summary>public 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./// </summary>public static LoadTask<T> Load<T>(string path, string typeHint = "", bool useSubThreads = false, ResourceLoader.CacheMode cacheMode = ResourceLoader.CacheMode.Reuse) where T : Resource{return new LoadTask<T>(path).Load(typeHint, useSubThreads, cacheMode) as LoadTask<T>;}}}

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; }}

        只要像这样,我们就可以在编辑器中创建,保存和修改该类型。

游戏流程思考 

        其实在复盘的相当长的时间内, 我很希望能把游戏流程抽象成可以被管理的对象,但是鉴于那难度之大,和不同游戏类型的流程差异太多,不利于框架复用。于是短时间内放弃了这一想法。

        转而研究了很多这种小东西,也算是受益匪浅。

结语

        其实开发了这么久,对游戏引擎的共性之间多少有些了解了,做得越久越发明白“引擎不重要”是什么意思,也越来越觉得清晰的设计思路比框架更重要。

        本来还有很多话但是到此为止吧,我的经验已经不够用了,也许下一次“杂谈”能更加侃侃而谈吧。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/60835.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Spring Boot框架:电商系统的设计与实现

摘 要 现代经济快节奏发展以及不断完善升级的信息化技术&#xff0c;让传统数据信息的管理升级为软件存储&#xff0c;归纳&#xff0c;集中处理数据信息的管理方式。本网上商城系统就是在这样的大环境下诞生&#xff0c;其可以帮助管理者在短时间内处理完毕庞大的数据信息&…

tensorflow案例6--基于VGG16的猫狗识别(准确率99.8%+),以及tqdm、train_on_batch的简介

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 前言 本次还是学习API和如何搭建神经网络为主&#xff0c;这一次用VGG16去对猫狗分类&#xff0c;效果还是很好的&#xff0c;达到了99.8% 文章目录 1、tqdm…

数字化转型企业架构设计手册(交付版),企业数字化转型建设思路、本质、数字化架构、数字化规划蓝图(PPT原件获取)

1、企业架构现状分析 2、企业架构内容框架 3、企业架构设计方法 3.1 、业务架构设计方法 3.2 、数据架构设计方法 3.3 、应用架构设计方法 3.4 、技术架构设计方法 软件全套资料部分文档清单&#xff1a; 工作安排任务书&#xff0c;可行性分析报告&#xff0c;立项申请审批表&…

Rust开发一个命令行工具(一,简单版持续更新)

依赖的包 cargo add clap --features derive clap命令行参数解析 项目目录 代码 main.rs mod utils;use clap::Parser; use utils::{editor::open_in_vscode,fs_tools::{file_exists, get_file, is_dir, list_dir, read_file}, }; /// 在文件中搜索模式并显示包含它的行。…

自动化运维(k8s):一键获取指定命名空间镜像包脚本

前言&#xff1a;脚本写成并非一蹴而就&#xff0c;需要不断的调式和修改&#xff0c;这里也是改到了7版本才在 生产环境 中验证成功。 该命令 和 脚本适用于以下场景&#xff1a;在某些项目中&#xff0c;由于特定的安全或政策要求&#xff0c;不允许连接到你的镜像仓库。然而…

Python学习笔记(1)装饰器、异常检测、标准库概览、面向对象

1 装饰器 装饰器&#xff08;decorators&#xff09;是 Python 中的一种高级功能&#xff0c;它允许你动态地修改函数或类的行为。 装饰器是一种函数&#xff0c;它接受一个函数作为参数&#xff0c;并返回一个新的函数或修改原来的函数。 语法使用 decorator_name 来应用在…

IntelliJ+SpringBoot项目实战(七)--在SpringBoot中整合Redis

Redis是项目开发中必不可少的缓存工具。所以在SpringBoot项目中必须整合Redis。下面是Redis整合的步骤&#xff1a; &#xff08;1&#xff09;因为目前使用openjweb-sys作为SpringBoot的启动应用&#xff0c;所以在openjweb-sys模块的application-dev.yml中增加配置参数&…

【UGUI】Unity 游戏开发:背包系统初始化道具教程

在游戏开发中&#xff0c;背包系统是一个非常常见的功能模块。它允许玩家收集、管理和使用各种道具。今天&#xff0c;我们将通过一个简单的示例来学习如何在 Unity 中初始化一个背包系统。我们将使用 Unity 2021.3.7 版本&#xff0c;并结合 C# 脚本来实现这一功能。 1. 场景…

AI工业大模型报告:体系架构、关键技术与典型应用

研究意义 随着新一代人工智能的发展, 大模型&#xff08;如 GPT-4o 等&#xff09;凭借大规模训练数据、网络参数和算 力涌现出强大的生成能力、泛化能力和自然交互能力, 展现出改变工业世界的巨大潜力. 尽管大模型 已在自然语言等多个领域取得突破性进展, 但其在工业应用中的…

电子电气架构 --- 电动汽车 800V 高压系统

我是穿拖鞋的汉子&#xff0c;魔都中坚持长期主义的汽车电子工程师。 老规矩&#xff0c;分享一段喜欢的文字&#xff0c;避免自己成为高知识低文化的工程师&#xff1a; 所有人的看法和评价都是暂时的&#xff0c;只有自己的经历是伴随一生的&#xff0c;几乎所有的担忧和畏惧…

shell编程之变量与引用

目录 深入认识变量什么是变量变量的名称变量数据类型变量的定义自定义变量环境变量位置变量 变量赋值和作用域赋值&#xff1a;变量名变量值read从键盘读入变量值变量和引号变量的作用域变量的运算 深入认识变量 什么是变量 变量是在程序中保存用户数据的一段内存存储空间&am…

UE5 材质里面画圆锯齿严重的问题

直接这么画圆会带来锯齿&#xff0c;我们对锯齿位置进行模糊 可以用smoothstep&#xff0c;做值的平滑过渡&#xff08;虽然不是模糊&#xff0c;但是类似&#xff09;

鸿蒙HarmonyOS开发:一次开发,多端部署(工程级)三层工程架构

文章目录 一、工程创建1、先创建出最基本的项目工程。2、新建common、features、 products 目录 二、工程结构三、依赖关系1、oh-package.json52、配置ohpm包依赖 四、引用ohpm包中的代码1、定义共享资源2、在common模块index文件中导出3、在phone模块oh-package.json5文件中引…

【笔记】关于git和GitHub和git bash

如何推送更新的代码到github仓库 如何在此项目已经提交在别的远程仓库的基础上更改远程仓库地址&#xff08;也就是换一个远程仓库提交&#xff09; 如何删除github中的一个文件 第二版 删除github上的一个仓库或者仓库里面的某个文件_github仓库删除一个文件好麻烦-CSDN博客 …

20241112-Pycharm使用托管的Anaconda的Jupyter Notebook

Pycharm使用托管的Anaconda的Jupyter Notebook 要求 不要每次使用 Pycharm 运行 Jupyter 文件时都要手动打开 Anaconda 的 Jupyter Notebook 正文 pycharm中配置好会自动安装的&#xff0c;有的要自己配置 Pycharm中配置 文件 ——> 设置 ——> 语言和框架……&am…

集合的介绍与比较器的应用

1.集合&#xff1a; 是一种容器&#xff0c;一种变量类型&#xff0c;跟数组很像 数组的缺点&#xff1a; A.数组的空间长度固定&#xff0c;一旦确定不可以更改。多了浪费&#xff0c;少了报错。 B.使用数组 操作数据的时候&#xff0c;【删除&#xff0c;增加】效率比较低。…

动态规划---解决多段图问题

ok 小伙伴们&#xff0c;我现在有点小小的红温&#xff0c;有点毛躁。 怎么解决多段图问题呢&#xff1f;求取最短路径有多种方法可取。 家人们&#xff0c;毫无思绪可言……………………………… 要实现动态规划&#xff0c;条件&#xff1a;子问题重叠度较高&#xff0c;并…

基于Spring Boot的在线性格测试系统设计与实现(源码+定制+开发)智能性格测试与用户个性分析平台、在线心理测评系统的开发、性格测试与个性数据管理系统

博主介绍&#xff1a; ✌我是阿龙&#xff0c;一名专注于Java技术领域的程序员&#xff0c;全网拥有10W粉丝。作为CSDN特邀作者、博客专家、新星计划导师&#xff0c;我在计算机毕业设计开发方面积累了丰富的经验。同时&#xff0c;我也是掘金、华为云、阿里云、InfoQ等平台…

cMake编译github中源码

https://github.com/gflags/gflags 记录一下防止遗忘&#xff0c;本次编译为gflags库 1.下载CMake-gui https://cmake.org/ 安装时&#xff0c;选择自动创建环境变量&#xff0c;安装完输入&#xff0c;查看是否安装成功 cmake --version 2.下载源码&#xff0c;解压打开文…

web——upload-labs——第四关——.htaccess文件绕过

先尝试直接上传一个普通的一句话木马 显示此文件不允许上传&#xff0c;这道题并没有提示不允许上传什么后缀的文件&#xff0c;经过尝试&#xff0c;基本上所有后缀能够被解析为php语句执行的文件都不能成功上传。试试正常的图片能不能上传&#xff1a; 我们再来试试图片马能不…