网站开发培训多少钱,郑州比较正规的装修公司,网站建设有云端吗,关于水果的网站开发虚幻本身有提供一些对异步操作的封装#xff0c;这里是对这段时间接触到的“非同步”的操作进行的总结。
当前使用的UE4版本为4.18.2。
在虚幻的游戏制作中#xff0c;如果不是特殊情况一般不会有用到线程的时候。但是由于实际上虚幻内部是有着许多线程机制的。
例如通常的…虚幻本身有提供一些对异步操作的封装这里是对这段时间接触到的“非同步”的操作进行的总结。
当前使用的UE4版本为4.18.2。
在虚幻的游戏制作中如果不是特殊情况一般不会有用到线程的时候。但是由于实际上虚幻内部是有着许多线程机制的。
例如通常的游戏引擎中游戏线程和渲染线程都是独立的相互之间会存在一个同步的机制。
而物理线程与游戏线程之间的同步有时候也会导致游戏的表现与预期不一致。
通常会有线程同步需求的地方是网络相关的操作但是实际上UE4已经对网络操作进行了封装无需关心这个问题。
而游戏线程、渲染线程、物理线程内部也都已经有了封装对游戏逻辑的构建基本是不可见的。
但是有时候还是会遇到需要使用线程相关逻辑的这里就是这段时间内累计的“非同步”相关逻辑的总结。
Tick
这个其实关于Tick的虽然Actor是有默认的Tick函数的Component与UMG也有对应的Tick机制。
但是如果是自定义的UObject或者Slate要使用Tick机制的话就会有些麻烦。
例如想要让自定义的Slate控件进行某种数据更新而数据源本身并不提供通知机制的话就会有些麻烦。
虽然通过各种设计可以巧妙的绕过这个问题但是有时候在类内部构建Tick机制才是最快速的解决方案。
TimerManager
通过使用引擎提供的定时器机制就可以进行自定义的Tick了 1 2 3 4 5 6 7 GetWord()-GetTimerManager().SetTimer( m_hTimerHandle, this, UNetPlayManager::TimerTick, 1.0, true );
这里需要能够获得UWorld的指针如果是自定义的类型的话就必须想办法提供有效的UWorld指针。
FTickableGameObject
还有另一个方法就是使用FTickableGameObject。
任何继承自FTickableGameObject的类型都会获得Tick的能力就算不是虚幻原生的类型也可以使用相当的便利。使用时继承自该类型然后 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public: /** Tick接口函数 */ virtual void Tick(float DeltaTime) override; virtual bool IsTickable() const override { return true; } virtual bool IsTickableWhenPaused() const override { return true; } virtual TStatId GetStatId() const override { RETURN_QUICK_DECLARE_CYCLE_STAT(USceneCapturer, STATGROUP_Tickables); }
继承一下基本的函数就可以了。
线程同步
UE4对操作系统提供的线程同步相关接口进行了一定的封装。
Atomics
基本的接口可以在FPlatformAtomics找到针对不同的平台有不同的实现。 InterlockedAdd InterlockedCompareExchange (-Pointer) InterlockedDecrement (-Increment) InterlockedExchange (-Pointer) 详细的可以参看其源码。也可以参看引擎内部的使用方式
class FThreadSafeCounter { public: int32 Add( int32 Amount ) { return FPlatformAtomics::InterlockedAdd(Counter, Amount); } private: volatile int32 Counter; }; 1 2 3 4 5 6 7 8 9 10 class FThreadSafeCounter { public: int32 Add( int32 Amount ) { return FPlatformAtomics::InterlockedAdd(Counter, Amount); } private: volatile int32 Counter; }; FCriticalSection
用于对非线程安全的区域进行保护。 1 FCriticalSection CriticalSection;
声明之后在需要的地方进行锁操作即可有提供作用域保护的封装
FScopeLock Lock(CriticalSection); 1 FScopeLock Lock(CriticalSection);
这样就不需要自己进行Lock和Unlock了可以有效的防止误操作导致的Bug的出现。
FSpinLock
锁操作提供LockUnlock以及BlockUntilUnlocked等便利的操作。
其实内部就是对FPlatformAtomics::InterlockedExchange的一个封装。
构造函数的InSpinTimeInSeconds就是默认的锁等待间隔默认值为0.1。
FSemaphore
这个是对信号量的封装但是似乎不建议使用。
而且并不是对于所有的平台都有实现的通常建议使用FEvent进行代替。
FEvent
这个相当于UE4封装的内部使用的互斥信号量机制有基本的等待和唤醒操作。
FScopedEvent
对FEvnet的封装在注释上能够看到使用示例 1 2 3 4 5 { FScopedEvent MyEvent; SendReferenceOrPointerToSomeOtherThread(MyEvent); // Other thread calls MyEvent-Trigger(); // MyEvent destructor is here, we wait here. }
这个操作就是将MyEvent发送到其他线程直到在其他的地方MyEvnet-Trigger()被调用为止都不会离开这个作用域继续执行。
容器
包括TArray, TMap在内的几乎大部分的容器都不是线程安全的需要自己对同步进行管理。
当然也能看到一些线程安全的封装例如TArrayWithThreadsafeAdd。
TLockFreePointerList
这个是一系列的类型在Task Graph系统中被使用到。如其名称是LockFree的。
TQueue
也是LockFree的在初始化时可以指定线程同步的类型EQueueMode分为Mpsc多生产者单消费者以及Spsc单生产者单消费者两种模式。
只有Spsc模式是contention free的。
仔细寻找的话UE4内部有实现很多便利的类型例如TCircularQueue这种针对双线程一个消费一个生产的线程安全类型。
工具类
FThreadSafeCounter
就是前面例子中的线程安全的计数器。
FThreadSingleton
为每一个线程创建一个实例。
FThreadIdleStats
用于统计线程空闲状态。
异步执行
UE4中对基本的线程操作进行了一定程度的封装使用相应的Helper就可以无需关心线程的创建这些问题。
AsyncTask
这个函数可以将一些简单的任务扔到UE4的线程池中去进行不必关心具体的线程同步问题。 1 2 3 4 5 6 7 8 9 10 11 if(IsInGameThread()) { //….一些操作 } else { AsyncTask(ENamedThreads::GameThread, []() { //….一些操作 }); }
其中第一个参数是发送到的线程的名称通常一些工作线程是无法执行引擎中IsGameThread()保护或者其他隐形的游戏线程代码的通过这个操作将其发送到游戏线程的话使用GameThread就可以了。
其实基本上的游戏逻辑中使用最多的就是这个函数了。
RHICmdList
这是一组独特的宏用于将操作发送到渲染线程进行操作。
主要是对Texture之类的数据在GPU以及GPU相关的指令进行执行。
例如 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 if (IsInRenderingThread()) { // Initialize the vertex factorys stream components. FDataType NewData; NewData.PositionComponent STRUCTMEMBER_VERTEXSTREAMCOMPONENT(InVertexBuffer, FPaperSpriteVertex, Position, VET_Float3); NewData.TangentBasisComponents[0] STRUCTMEMBER_VERTEXSTREAMCOMPONENT(InVertexBuffer, FPaperSpriteVertex, TangentX, VET_PackedNormal); NewData.TangentBasisComponents[1] STRUCTMEMBER_VERTEXSTREAMCOMPONENT(InVertexBuffer, FPaperSpriteVertex, TangentZ, VET_PackedNormal); NewData.ColorComponent STRUCTMEMBER_VERTEXSTREAMCOMPONENT(InVertexBuffer, FPaperSpriteVertex, Color, VET_Color); NewData.TextureCoordinates.Add(FVertexStreamComponent(InVertexBuffer, STRUCT_OFFSET(FPaperSpriteVertex, TexCoords), sizeof(FPaperSpriteVertex), VET_Float2)); SetData(NewData); } else { ENQUEUE_UNIQUE_RENDER_COMMAND_TWOPARAMETER( InitPaperSpriteVertexFactory, FPaperSpriteVertexFactory*, VertexFactory, this, const FPaperSpriteVertexBuffer*, VB, InVertexBuffer, { VertexFactory-Init(VB); }); }
这样就可以保证只能在渲染线程执行的代码不会被其他线程执行到。
渲染线程还有一些需要注意的是UE4中有的代码的执行其实是在渲染线程中的如果没有留意的话会造成隐形的线程同步问题。例如通常UMG的OnPaint。
FAsyncTask
这个是一组任务的封装类是基本的任务单元最简单的使用如下
FAutoDeleteAsyncTask 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class ExampleAutoDeleteAsyncTask : public FNonAbandonableTask { friend class FAutoDeleteAsyncTaskExampleAutoDeleteAsyncTask; int32 ExampleData; ExampleAutoDeleteAsyncTask(int32 InExampleData) : ExampleData(InExampleData) { UE_LOG(LogTemp, Log, TEXT([ExampleAutoDeleteAsyncTask] Construct())); } void DoWork() { UE_LOG(LogTemp, Log, TEXT([ExampleAutoDeleteAsyncTask] DoWork())); } FORCEINLINE TStatId GetStatId() const { RETURN_QUICK_DECLARE_CYCLE_STAT(ExampleAutoDeleteAsyncTask, STATGROUP_ThreadPoolAsyncTasks); } };
在完成定义后可以有两种使用方式 1 2 3 4 5 // 将任务扔到线程池中去执行 (new FAutoDeleteAsyncTaskExampleAutoDeleteAsyncTask(5))-StartBackgroundTask(); // 直接在当前线程执行操作 (new FAutoDeleteAsyncTaskExampleAutoDeleteAsyncTask(5))-StartSynchronousTask();
FAutoDeleteAsyncTask的一个优点是在执行完成后会自动销毁无需进行额外的关注。通常文件写入或者压缩数据之类的无须进行过程管理的操作可以交付给他执行。
FAsyncTask
这个才是本尊由于不会自动删除有需要进行额外操作的情况。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 MyTask-StartSynchronousTask(); //to just do it now on this thread //Check if the task is done : if (MyTask-IsDone()) { } //Spinning on IsDone is not acceptable( see EnsureCompletion ), but it is ok to check once a frame. //Ensure the task is done, doing the task on the current thread if it has not been started, waiting until completion in all cases. MyTask-EnsureCompletion(); delete Task;
但是如果是使用StartBackgroundTask()的话依然不需要自己进行管理。
FRunnable
这个是交付给线程的执行体封装通常用于比AsyncTask更加复杂的操作。
分为Init(), Run(), Exit()三个操作如果Init失败就不会执行Run()Run()执行完成就会执行Exit()。 1 2 3 4 5 6 7 8 9 10 11 class FRunAbleTest : public FRunnable { virtual uint32 Run() override { UE_LOG(LogTemp, Log, TEXT([FRunAbleTest] Run())); FPlatformProcess::Sleep(30); UE_LOG(LogTemp, Log, TEXT([FRunAbleTest] Run(): Comp)); return 0; } };
通常也可以只指定Run()然后交付给线程 1 2 FRunnable* tp_Runable new FRunAbleTest(); mp_TestThread FRunnableThread::Create(tp_Runable, TEXT(Test_01));
就可以了。
Async
这是另一个异步执行的宏与AsyncTask有少许不同。
Async的简单的使用方式在注释中有提到 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // 使用全局函数 int TestFunc() { return 123; } TFunctionint() Task TestFunc(); auto Result Async(EAsyncExecution::Thread, Task); // 使用lambda TFunctionint() Task []() { return 123; } auto Result Async(EAsyncExecution::Thread, Task); // 使用inline lambda auto Result Asyncint(EAsyncExecution::Thread, []() { return 123; }
第一个参数为执行的类型TaskGraph是将其放到任务图中去执行Thread则是在单独的线程中执行TreadPool则是放入线程池中去执行。
这里并不能像AsyncTask一样指定目标的线程。
同时Async会返回一个TFutureResultType而ResultType则是传入的执行函数的返回值。 1 2 3 4 5 6 TFunctionint() My_Task []() { return 123; }; auto Future Async(EAsyncExecution::TaskGraph, My_Task); int Result Future.Get();
类似这样的调用即可。
总结
UE4提供的异步操作大体上分为TaskGraph和TreadPool的管理方式通常较简单的任务交付给TaskGraph复杂的任务交付给Thread。
对于Task引擎会有自己的管理将其分配给空闲的Worker Thread。同时Task之间的依赖关系也会被管理并按照需要的顺序被执行。
其实TaskGroup和ThreadPool都是可以自己进行申请和管理的但是并没有实际的进行研究。
因为理论上除非有需求应当尽量的让游戏逻辑保持简洁。再加上线程同步是要支付额外的成本的因此要尽量避免对异步逻辑的使用即使使用也要尽量的保持逻辑单纯。而且这两个系统本身是虚幻为编辑器而设计的虽然开放给用户使用但是就像GamePlayAbility系统一样。本身每个程序员都有自己的实现思路也没有必要一定要使用这套系统。
毕竟游戏最终是用户体验没有用户在意屏幕背后的逻辑实现是否”Geek”。