Unreal Engine MCP
EpicGames/unreal-engine-skills-for-claude-code-plugin
Drives a live Unreal Editor through the unreal-mcp server: spawn actors, edit Blueprints, materials and sequences, change properties and trigger Live Coding.
Picks and applies the right Unreal Engine C++ concurrency API, from UE::Tasks and FPipe to ParallelFor, FRunnable, locks and timers, with game-thread safety rules.
$ npx skills add quodsoler/unreal-engine-skills --skill ue-async-threading -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install quodsoler/unreal-engine-skills ue-async-threading --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/quodsoler/unreal-engine-skills.git skills-src && mkdir -p .claude/skills && cp -r skills-src/skills/ue-async-threading .claude/skills/ue-async-threading && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "ue-async-threading" agent skill from https://github.com/quodsoler/unreal-engine-skills/tree/main/skills/ue-async-threading into .claude/skills/ue-async-threading/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "ue-async-threading", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/quodsoler/unreal-engine-skills/tree/main/skills/ue-async-threadingType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add quodsoler/unreal-engine-skills --skill ue-async-threading -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install quodsoler/unreal-engine-skills ue-async-threading --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/quodsoler/unreal-engine-skills.git skills-src && mkdir -p .agents/skills && cp -r skills-src/skills/ue-async-threading .agents/skills/ue-async-threading && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "ue-async-threading" agent skill from https://github.com/quodsoler/unreal-engine-skills/tree/main/skills/ue-async-threading into .agents/skills/ue-async-threading/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "ue-async-threading", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add quodsoler/unreal-engine-skills --skill ue-async-threading -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install quodsoler/unreal-engine-skills ue-async-threading --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/quodsoler/unreal-engine-skills.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/skills/ue-async-threading .cursor/skills/ue-async-threading && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "ue-async-threading" agent skill from https://github.com/quodsoler/unreal-engine-skills/tree/main/skills/ue-async-threading into .cursor/skills/ue-async-threading/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "ue-async-threading", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/quodsoler/unreal-engine-skills.git --path skills/ue-async-threading--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add quodsoler/unreal-engine-skills --skill ue-async-threading -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install quodsoler/unreal-engine-skills ue-async-threading --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/quodsoler/unreal-engine-skills.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/skills/ue-async-threading .gemini/skills/ue-async-threading && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "ue-async-threading" agent skill from https://github.com/quodsoler/unreal-engine-skills/tree/main/skills/ue-async-threading into .gemini/skills/ue-async-threading/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "ue-async-threading", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install quodsoler/unreal-engine-skills ue-async-threadingInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add quodsoler/unreal-engine-skills --skill ue-async-threading -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/quodsoler/unreal-engine-skills.git skills-src && mkdir -p .github/skills && cp -r skills-src/skills/ue-async-threading .github/skills/ue-async-threading && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "ue-async-threading" agent skill from https://github.com/quodsoler/unreal-engine-skills/tree/main/skills/ue-async-threading into .github/skills/ue-async-threading/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "ue-async-threading", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add quodsoler/unreal-engine-skills --skill ue-async-threading -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install quodsoler/unreal-engine-skills ue-async-threading --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/quodsoler/unreal-engine-skills.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/skills/ue-async-threading .opencode/skills/ue-async-threading && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "ue-async-threading" agent skill from https://github.com/quodsoler/unreal-engine-skills/tree/main/skills/ue-async-threading into .opencode/skills/ue-async-threading/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "ue-async-threading", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
ue-async-threadingPicks and applies the right Unreal Engine C++ concurrency API, from UE::Tasks and FPipe to ParallelFor, FRunnable, locks and timers, with game-thread safety rules.
The skill covers moving work off the game thread in Unreal Engine C++ and bringing results back, targeting UE 5.8 with APIs checked against those headers. It spans the UE::Tasks system and FPipe, Async with TFuture and TPromise, thread-pool FAsyncTask work, ParallelFor loops, dedicated FRunnable threads, locks and lock-free queues, and tickers and timers. A routing table sends each kind of request to the right section, and a threading-model table shows which thread owns what, such as the game thread for all UObject access and the render thread for render commands.
Module notes explain that most of this lives in Core, timers in Engine and render commands in RenderCore, with the Build.cs dependencies to add. The agent reads .agents/ue-project-context.md when it exists, asks only when two readings would produce different code, and avoids the older API forms listed in a deprecated section. Two reference files cover thread safety and threading patterns, and async asset loading and smart-pointer lifetime are handed to the ue-data-assets-tables and ue-cpp-foundations skills.
6 steps, taken from the first numbered list in SKILL.md.
Read from SKILL.md and the folder at commit f3742d7. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
No scripts in the folder and no shell commands in SKILL.md (its code samples are cpp).
From the folder's file list and the shell code blocks in SKILL.md.
No URLs in SKILL.md.
From URLs in SKILL.md, links to its own repository left out.
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Unreal Engine Async and Threading loads about 8k tokens when it runs, and up to ~16k if it reads all its reference files. Until then it costs about 141 tokens; SKILL.md has 1,803 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check found no risky patterns in SKILL.md.
Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from quodsoler/unreal-engine-skills at commit f3742d7, republished under its MIT licence (© quodsoler). 1,803 words, ~7,971 tokens.
.claude/skills/ue-async-threading/SKILL.md (or your agent's skills folder). This skill also uses 2 other files; get the full folder from GitHub.Target engine: UE 5.8. APIs below are verified against the 5.8 headers; older forms are listed under "Deprecated — do not use".
This skill covers moving work off the game thread and bringing results back: the UE::Tasks system (Tasks/Task.h, Tasks/Pipe.h), Async/TFuture (Async/Async.h, Async/Future.h), thread-pool FAsyncTask (Async/AsyncWork.h), ParallelFor, dedicated FRunnable threads, locks and lock-free queues, FTSTicker and FTimerManager. Everything except timers is in the Core module; FTimerManager is in Engine; ENQUEUE_RENDER_COMMAND is in RenderCore. Build.cs: PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine" }); and add "RenderCore" only when enqueuing render commands.
Read .agents/ue-project-context.md if it exists (module names, conventions, enabled plugins, GAS/networking setup). Do not stop if it is missing.
Identify the area from the request and the codebase. Ask only when two plausible readings would produce different code.
| Request is about… | Go to |
|---|---|
| Which API fits | Pattern Selection |
| One-shot background work, chaining, prerequisites, events | UE::Tasks |
| Serializing access to a resource without a dedicated thread | FPipe and FTaskConcurrencyLimiter |
| Futures, promises, dispatch to the game thread | Async, TFuture and AsyncTask |
| Reusable pooled work unit | FAsyncTask and FAutoDeleteAsyncTask |
| Data-parallel loops | ParallelFor |
| Long-lived dedicated thread | FRunnable and FRunnableThread |
| Locks, events, atomics, queues, shared pointers | Synchronization |
| Per-frame callbacks and delayed calls | Tickers and Timers |
| UObject, GC and render-thread rules | Thread Safety Rules |
| Old forms | Deprecated — do not use |
| Thread | Check | Owns |
|---|---|---|
| Game thread | IsInGameThread() (CoreGlobals.h) | All UObject access, Blueprint, gameplay, timers, tickers |
| Render thread | IsInRenderingThread() | Scene proxies, render commands (ENQUEUE_RENDER_COMMAND) |
| RHI thread | IsInRHIThread() | GPU command submission |
| Worker threads | none | UE::Tasks scheduler, TaskGraph AnyThread, ParallelFor |
| Thread pools | none | GThreadPool, GBackgroundPriorityThreadPool, GIOThreadPool; GLargeThreadPool only WITH_EDITOR (Misc/QueuedThreadPool.h) |
Golden rule: UObjects are game-thread-only. Compute off-thread on plain data, then apply results on the game thread through a TWeakObjectPtr (see Thread Safety Rules).
| Need | Use | Result |
|---|---|---|
| One-shot background work, dependencies, chaining | UE::Tasks::Launch | TTask<T> |
| Run a lambda on the game thread from anywhere | AsyncTask(ENamedThreads::GameThread, ...) | none |
| Future-style result with execution-context choice | Async(EAsyncExecution, ...) | TFuture<T> |
| Serialize tasks touching one resource (FIFO) | UE::Tasks::FPipe | TTask<T> |
| Cap how many tasks run at once | UE::Tasks::FTaskConcurrencyLimiter | none |
| Reusable pooled work unit with owner-managed lifetime | FAsyncTask<T> / FAutoDeleteAsyncTask<T> | via GetTask() |
| Data-parallel loop, caller blocks | ParallelFor | none |
| Long-lived thread (socket, file watcher, sim loop) | FRunnable + FRunnableThread::Create | manual |
| Per-frame callback outside an Actor tick | FTSTicker::GetCoreTicker().AddTicker | handle |
| Delayed or repeating call on the game thread | FTimerManager::SetTimer | FTimerHandle |
#include "Tasks/Task.h"
// Tasks/Task.h:299
template<typename TaskBodyType>
UE::Tasks::TTask<TInvokeResult_T<TaskBodyType>> Launch(const TCHAR* DebugName, TaskBodyType&& TaskBody,
ETaskPriority Priority = ETaskPriority::Normal,
EExtendedTaskPriority ExtendedPriority = EExtendedTaskPriority::None,
ETaskFlags Flags = ETaskFlags::None);
// Tasks/Task.h:324 — same, with a prerequisites collection as the third parameter
template<typename TaskBodyType, typename PrerequisitesCollectionType>
UE::Tasks::TTask<TInvokeResult_T<TaskBodyType>> Launch(const TCHAR* DebugName, TaskBodyType&& TaskBody,
PrerequisitesCollectionType&& Prerequisites,
ETaskPriority Priority = ETaskPriority::Normal,
EExtendedTaskPriority ExtendedPriority = EExtendedTaskPriority::None,
ETaskFlags Flags = ETaskFlags::None);using namespace UE::Tasks;
TArray<int32> Data;
TTask<int32> Sum = Launch(UE_SOURCE_LOCATION, [Data]() { return ComputeSum(Data); });
// Prerequisites: TaskB runs after TaskA completes. Handles are copyable; GetResult() is non-const.
TTask<FVector> TaskA = Launch(UE_SOURCE_LOCATION, []() { return FVector(1.f, 2.f, 3.f); });
TTask<void> TaskB = Launch(UE_SOURCE_LOCATION,
[TaskA]() mutable { const FVector Pos = TaskA.GetResult(); ConsumePosition(Pos); },
Prerequisites(TaskA), ETaskPriority::BackgroundNormal);
// Manual gate: nothing after the event starts until Trigger()
FTaskEvent Gate{ UE_SOURCE_LOCATION };
TTask<void> Gated = Launch(UE_SOURCE_LOCATION, []() { DoWork(); }, Prerequisites(Gate));
Gate.Trigger();
// Wait for a group, with timeout (Tasks/Task.h:393)
TArray<FTask> Group{ TaskB, Gated };
const bool bAllDone = Wait(Group, FTimespan::FromMilliseconds(5.0));
// Cooperative cancellation (Tasks/Task.h: FCancellationToken)
FCancellationToken Token;
Launch(UE_SOURCE_LOCATION, [&Token]() { for (int32 i = 0; i < 1000; ++i) { if (Token.IsCanceled()) { return; } Step(i); } });
Token.Cancel();
// Already-completed task holding a value (useful for uniform interfaces)
TTask<int32> Ready = MakeCompletedTask<int32>(42);Handle API (Tasks/Task.h:36-95): IsValid(), IsCompleted(), Wait(), Wait(FTimespan Timeout) returns bool, TryRetractAndExecute() runs the task inline if not started, GetResult() waits then returns ResultType& (check(IsValid())). Free functions: Wait(FTask&), Wait(Collection, FTimespan), WaitAny(Collection, Timeout) returns the index, Any(Collection) returns an FTask completed when any input completes, AddNested(Task) from inside a running task so the parent is not complete until the nested one is.
Priorities (Async/Fundamental/Task.h:20, Tasks/TaskPrivate.h:59-93): ETaskPriority::High | Normal (= Default) | BackgroundHigh | BackgroundNormal | BackgroundLow | Inherit. EExtendedTaskPriority::None | Inline | TaskEvent | GameThreadNormalPri | GameThreadHiPri | GameThreadNormalPriLocalQueue | GameThreadHiPriLocalQueue | RenderThreadNormalPri | RenderThreadHiPri | RHIThreadNormalPri | RHIThreadHiPri (plus LocalQueue variants). ETaskFlags::None | DoNotRunInsideBusyWait. Passing EExtendedTaskPriority::GameThreadNormalPri runs the body on the game thread.
A legacy TaskGraph FGraphEventRef can be passed directly as the prerequisites argument of Launch (Tasks/TaskPrivate.h:266), and collections of them work in WaitAny/Any (Tasks/Task.h:417, 468), so UE::Tasks can wait on TaskGraph work. Full chained example: threading-patterns.md.
FPipe executes its tasks one after another (FIFO when no extra prerequisites), so it replaces a dedicated thread guarding a resource. The pipe must outlive its last task; ~FPipe() asserts !HasWork().
#include "Tasks/Pipe.h"
#include "Tasks/TaskConcurrencyLimiter.h"
UE::Tasks::FPipe SavePipe{ TEXT("SavePipe") }; // explicit FPipe(const TCHAR* InDebugName)
UE::Tasks::TTask<void> First = SavePipe.Launch(UE_SOURCE_LOCATION, []() { WriteChunk(0); });
UE::Tasks::TTask<bool> Second = SavePipe.Launch(UE_SOURCE_LOCATION, []() { return WriteChunk(1); },
UE::Tasks::ETaskPriority::BackgroundNormal);
const bool bInsidePipe = SavePipe.IsInContext(); // true only while a pipe task runs on this thread
SavePipe.WaitUntilEmpty(); // before destroying the pipe
UE::Tasks::FTaskConcurrencyLimiter Limiter(4 /*MaxConcurrency*/, UE::Tasks::ETaskPriority::BackgroundHigh);
for (int32 Index = 0; Index < 64; ++Index)
{
Limiter.Push(UE_SOURCE_LOCATION, [Index](uint32 Slot) { ProcessWithScratch(Index, Slot); }); // Slot in [0, MaxConcurrency)
}
Limiter.Wait(); // Wait(FTimespan Timeout = FTimespan::MaxValue())FPipe::Launch(const TCHAR*, TaskBody, [Prerequisites,] ETaskPriority = Default, EExtendedTaskPriority = None, ETaskFlags = None) (Tasks/Pipe.h:63,90). FTaskConcurrencyLimiter may be destroyed before its tasks finish; its Wait is satisfied once and never re-arms (Tasks/TaskConcurrencyLimiter.h:171-212).
#include "Async/Async.h"
// Async/Async.h:299
template<typename CallableType>
auto Async(EAsyncExecution Execution, CallableType&& Callable, TUniqueFunction<void()> CompletionCallback = nullptr) -> TFuture<decltype(Forward<CallableType>(Callable)())>;
// Async/Async.h:407 — takes a reference, so pass *GThreadPool
template<typename CallableType>
auto AsyncPool(FQueuedThreadPool& ThreadPool, CallableType&& Callable, TUniqueFunction<void()> CompletionCallback = nullptr, EQueuedWorkPriority InQueuedWorkPriority = EQueuedWorkPriority::Normal);
// Async/Async.h:430
template<typename CallableType>
auto AsyncThread(CallableType&& Callable, uint32 StackSize = 0, EThreadPriority ThreadPri = TPri_Normal, TUniqueFunction<void()> CompletionCallback = nullptr);
// Async/Async.h:463
CORE_API void AsyncTask(ENamedThreads::Type Thread, TUniqueFunction<void()> Function);EAsyncExecution (Async/Async.h:27) | Runs on |
|---|---|
TaskGraph | Worker thread, short tasks |
TaskGraphMainThread | Game thread, may run inside GC or PostLoad waits — only for code safe anywhere |
TaskGraphMainTick | Game thread inside a Tick — the safe choice for delegates and UObject code |
Thread | New dedicated thread, long-running or blocking I/O |
ThreadIfForkSafe | As Thread, fork-aware |
ThreadPool | GThreadPool |
LargeThreadPool | GLargeThreadPool, WITH_EDITOR only |
TFuture<FMyResult> Future = Async(EAsyncExecution::ThreadPool, []() { return ComputeResult(); });
if (Future.IsReady()) { UseResult(Future.Get()); } // non-blocking check
FMyResult Copy = Future.Get(); // blocks; does NOT invalidate (Async/Future.h:226)
Future.Next([](FMyResult Value) { UseResult(Value); }); // continuation receives the value; runs on the completing thread
TPromise<FMyResult> Promise;
TFuture<FMyResult> FromPromise = Promise.GetFuture(); // call once
Async(EAsyncExecution::Thread, [P = MoveTemp(Promise)]() mutable { P.SetValue(ComputeResult()); });
AMyActor* MyActor = FindMyActor();
AsyncTask(ENamedThreads::GameThread, [WeakActor = TWeakObjectPtr<AMyActor>(MyActor), Copy]()
{
if (AMyActor* Actor = WeakActor.Get()) { Actor->ApplyResult(Copy); }
});TFuture<T> (Async/Future.h:210-440): Get(), IsReady(), IsValid(), Wait(), WaitFor(const FTimespan&), WaitUntil(const FDateTime&), Then(Func) receives TFuture<T>, Next(Func) receives T (both move the state out and invalidate this future, Async/Future.h:669), Consume() moves the value out and invalidates, Share() gives TSharedFuture<T>, Reset(). TPromise<T> (:527): GetFuture(), SetValue(const T&), SetValue(T&&), EmplaceValue(Args&&...). Continuations run on whichever thread completes the promise — hop to the game thread explicitly.
Reusable work unit on a FQueuedThreadPool. Subclass FNonAbandonableTask (Async/AsyncWork.h:666), implement DoWork() and GetStatId(). FAsyncTask<T> constructs T inside its own constructor, so with the friend declaration T's constructor may be private (the engine's own example in AsyncWork.h:26-42 does this). Members your code reads through Task->GetTask() must be public, because that access happens outside the friend.
#include "Async/AsyncWork.h"
class FMyChunkTask : public FNonAbandonableTask
{
public:
friend class FAsyncTask<FMyChunkTask>;
friend class FAutoDeleteAsyncTask<FMyChunkTask>;
explicit FMyChunkTask(TArray<int32> InInput) : Input(MoveTemp(InInput)) {}
int32 Result = 0;
void DoWork()
{
for (int32 Value : Input) { Result += Value; }
}
FORCEINLINE TStatId GetStatId() const
{
RETURN_QUICK_DECLARE_CYCLE_STAT(FMyChunkTask, STATGROUP_ThreadPoolAsyncTasks);
}
private:
TArray<int32> Input;
};
// Owner-managed lifetime
TArray<int32> Numbers;
FAsyncTask<FMyChunkTask>* Task = new FAsyncTask<FMyChunkTask>(MoveTemp(Numbers));
Task->StartBackgroundTask(); // (FQueuedThreadPool* = GThreadPool, EQueuedWorkPriority = Normal, EQueuedWorkFlags = None, int64 RequiredMemory = -1, const TCHAR* DebugName = nullptr)
const bool bReady = Task->IsDone(); // poll once per frame, never spin
Task->EnsureCompletion(); // (bool bDoWorkOnThisThreadIfNotStarted = true, bool bIsLatencySensitive = false)
const int32 Sum = Task->GetTask().Result;
delete Task;
// Fire-and-forget: deletes itself after DoWork
(new FAutoDeleteAsyncTask<FMyChunkTask>(MoveTemp(Numbers)))->StartBackgroundTask();Other members (Async/AsyncWork.h:415-558): StartSynchronousTask(...) runs inline; Cancel() returns true if it was still queued; WaitCompletionWithTimeout(float TimeLimitSeconds); IsWorkDone() is the cheap non-blocking check. Pass GBackgroundPriorityThreadPool as the pool for low-priority work. Full template: threading-patterns.md.
#include "Async/ParallelFor.h"
// Async/ParallelFor.h:526, :543
inline void ParallelFor(int32 Num, TFunctionRef<void(int32)> Body, EParallelForFlags Flags = EParallelForFlags::None);
inline void ParallelFor(const TCHAR* DebugName, int32 Num, int32 MinBatchSize, TFunctionRef<void(int32)> Body, EParallelForFlags Flags = EParallelForFlags::None);
// Async/ParallelFor.h:792 — Body is called as Body(ContextType&, int32 Index)
template <typename ContextType, typename ContextAllocatorType, typename FunctionType>
inline void ParallelForWithTaskContext(const TCHAR* DebugName, TArray<ContextType, ContextAllocatorType>& OutContexts, int32 Num, int32 MinBatchSize, const FunctionType& Body, EParallelForFlags Flags = EParallelForFlags::None);TArray<UStaticMesh*> Meshes;
ParallelFor(Meshes.Num(), [&Meshes](int32 Index) { ProcessMesh(Meshes[Index]); });
ParallelFor(TEXT("ProcessMeshes"), Meshes.Num(), 64, [&Meshes](int32 Index) { ProcessMesh(Meshes[Index]); },
EParallelForFlags::Unbalanced | EParallelForFlags::BackgroundPriority);
struct FMyScratch { TArray<FVector> Buffer; };
TArray<FMyScratch> Contexts; // one per worker task, reused across iterations
ParallelForWithTaskContext(TEXT("Normals"), Contexts, Meshes.Num(), 32,
[&Meshes](FMyScratch& Scratch, int32 Index) { Scratch.Buffer.Reset(); ComputeNormals(Meshes[Index], Scratch.Buffer); });EParallelForFlags (Async/ParallelFor.h:46) | Effect |
|---|---|
None | Default |
ForceSingleThread | Run sequentially on the caller (debugging) |
Unbalanced | Iterations have very different costs; smaller batches |
PumpRenderingThread | Caller pumps render commands while waiting |
BackgroundPriority | Workers run at background priority |
Also available: ParallelForTemplate(...) (no TFunctionRef indirection, :496), ParallelForWithPreWork(...) (:571, run caller-side work before helping), ParallelForWithTaskContext(OutContexts, Num, ContextConstructor, Body, Flags) (:722), ParallelForWithExistingTaskContext(TArrayView<ContextType> Contexts, Num, MinBatchSize, Body, Flags) (:815). The caller participates and blocks until every iteration finishes. CVar Async.ParallelFor.DisableOversubscription (GParallelForDisableOversubscription, Async/ParallelFor.h:43) stops ParallelFor from waking extra workers.
Use only for a dedicated, long-lived thread. Lifecycle on the new thread: Init() → Run() → Exit(). Stop() is called from outside by Kill(); it must only signal.
// HAL/Runnable.h:32-69 — override verbatim
virtual bool Init();
virtual uint32 Run() = 0;
virtual void Stop();
virtual void Exit();
virtual class FSingleThreadRunnable* GetSingleThreadInterface(); // return a fallback for -nothreading platforms, or nullptr
// HAL/RunnableThread.h:44
static CORE_API FRunnableThread* Create(
class FRunnable* InRunnable,
const TCHAR* ThreadName,
uint32 InStackSize = 0,
EThreadPriority InThreadPri = TPri_Normal,
uint64 InThreadAffinityMask = FPlatformAffinity::GetNoAffinityMask(),
EThreadCreateFlags InCreateFlags = EThreadCreateFlags::None);
virtual bool Kill(bool bShouldWait = true) = 0; // calls Stop(); with bShouldWait blocks until Run() returns
virtual void WaitForCompletion() = 0;
virtual void Suspend(bool bShouldPause = true) = 0;
virtual void SetThreadPriority(EThreadPriority NewPriority) = 0;EThreadPriority (GenericPlatform/GenericPlatformAffinity.h:25): TPri_Normal, TPri_AboveNormal, TPri_BelowNormal, TPri_Highest, TPri_Lowest, TPri_SlightlyBelowNormal, TPri_TimeCritical. Always Kill(true) then delete the FRunnableThread*; killing without waiting leaks and can deadlock (header comment HAL/RunnableThread.h:77-79). Sleep with FPlatformProcess::Sleep(float Seconds) or block on an FEventRef instead of spinning. Full template with shutdown: threading-patterns.md.
| Need | Type | RAII guard | Header |
|---|---|---|---|
| General mutex, recursive | FCriticalSection (= UE::FPlatformRecursiveMutex) | FScopeLock Lock(&Mutex); FScopeUnlock to release inside a scope | HAL/CriticalSection.h:53, Misc/ScopeLock.h:140 |
| Many readers, one writer, not recursive | FRWLock (= UE::FPlatformRWLock) | FReadScopeLock(FRWLock&), FWriteScopeLock(FRWLock&), FRWScopeLock(Lock, SLT_ReadOnly / SLT_Write) | HAL/CriticalSection.h:56, Misc/ScopeRWLock.h:92-198 |
| One-byte, non-recursive, unfair, fastest | UE::FMutex | UE::TUniqueLock<UE::FMutex>; UE::TDynamicUniqueLock with UE::DeferLock | Async/Mutex.h:18, Async/UniqueLock.h:19,48, Async/LockTags.h:12 |
| Small recursive mutex | UE::FRecursiveMutex | UE::TUniqueLock | Async/RecursiveMutex.h:19 |
| Small readers/writer | UE::FSharedMutex (LockShared/UnlockShared) | UE::TSharedLock, UE::TUniqueLock | Async/SharedMutex.h:22, Async/SharedLock.h:21 |
Guard any type with Lock()/Unlock() | UE::TScopeLock<MutexType> | — | Misc/ScopeLock.h:25 |
TArray<FVector> Points;
mutable FCriticalSection Mutex; // mutable so const getters can lock
void Add(const FVector& P) { FScopeLock Lock(&Mutex); Points.Add(P); }
TMap<FName, FVector> Cache;
mutable FRWLock CacheLock;
FVector Read(FName Key) const { FReadScopeLock Lock(CacheLock); return Cache.FindRef(Key); }
void Write(FName Key, FVector V) { FWriteScopeLock Lock(CacheLock); Cache.Add(Key, V); }
int32 Counter = 0;
UE::FMutex SmallMutex;
void Bump() { UE::TUniqueLock Lock(SmallMutex); ++Counter; }Events: FEventRef Event(EEventMode::AutoReset) (HAL/Event.h:129-139) is the RAII pooled FEvent: Event->Trigger(), Event->Wait(), Event->Wait(uint32 WaitTimeMs), Event->Reset(). Raw pooling: FEvent* E = FPlatformProcess::GetSynchEventFromPool(bool bIsManualReset = false) / FPlatformProcess::ReturnSynchEventToPool(E) (GenericPlatformProcess.h:786,799). UE::FManualResetEvent (Async/ManualResetEvent.h): Notify(), Wait(), WaitFor(FMonotonicTimeSpan), Reset(). Between tasks prefer UE::Tasks::FTaskEvent — waiting on it does not block a worker.
Atomics: std::atomic<T> (<atomic>, already included by Templates/Atomic.h). FThreadSafeCounter, FThreadSafeBool and TAtomic are marked deprecated in their headers (see Deprecated). Use std::memory_order_relaxed for pure flags and counters, acquire/release when the atomic publishes other data.
Queues: TMpscQueue<T> (Containers/MpscQueue.h) and TSpscQueue<T> (Containers/SpscQueue.h): Enqueue(Args&&...), bool Dequeue(T& Out), TOptional<T> Dequeue(), T* Peek(), IsEmpty(). Single consumer only. TQueue<T, EQueueMode> still compiles but is marked "planned for deprecation" (Containers/Queue.h:11).
Shared pointers: TSharedPtr, TSharedRef, TWeakPtr and MakeShared default to ESPMode::ThreadSafe (Templates/SharedPointerFwd.h:24-27, SharedPointer.h:2110); the refcount is atomic, the pointee is not protected. Opt into ESPMode::NotThreadSafe only for hot single-thread paths.
Both run on the game thread. FTSTicker is engine-wide and survives level changes; FTimerManager is per UWorld and pauses with it.
#include "Containers/Ticker.h"
// Inside AMyActor, which declares: void Poll(float DeltaTime); void OnFire();
// Containers/Ticker.h:45,56,66 — delegate returns true to keep ticking, false to remove itself
FTSTicker::FDelegateHandle TickHandle = FTSTicker::GetCoreTicker().AddTicker(
FTickerDelegate::CreateWeakLambda(this, [this](float DeltaTime) { Poll(DeltaTime); return true; }), 0.0f /*InDelay*/);
FTSTicker::FDelegateHandle Named = FTSTicker::GetCoreTicker().AddTicker(TEXT("MyPoll"), 0.5f, [](float DeltaTime) { return true; });
FTSTicker::RemoveTicker(TickHandle); // static; safe with an expired handleFTickerDelegate is DECLARE_DELEGATE_RetVal_OneParam(bool, FTickerDelegate, float) (Containers/Ticker.h:21). Subclass FTSTickerObjectBase and override virtual bool Tick(float DeltaTime) = 0 for an object that registers itself (:136-158).
#include "TimerManager.h"
// Engine/Public/TimerManager.h:167-237, 247-268, 281-291
FTimerHandle FireHandle, OnceHandle, DelegateHandle; // normally UPROPERTY-free members of AMyActor
FTimerManager& Timers = GetWorldTimerManager(); // AActor; elsewhere GetWorld()->GetTimerManager()
Timers.SetTimer(FireHandle, this, &AMyActor::OnFire, 1.0f, /*InbLoop*/ true, /*InFirstDelay*/ -1.f);
Timers.SetTimer(OnceHandle, FTimerDelegate::CreateWeakLambda(this, [this]() { OnFire(); }), 2.0f, false);
Timers.SetTimer(DelegateHandle, FTimerDelegate::CreateUObject(this, &AMyActor::OnFire), 1.0f, false, 0.25f);
FTimerManagerTimerParameters Params; // TimerManager.h:124
Params.bLoop = true; Params.bMaxOncePerFrame = true; Params.FirstDelay = 0.5f;
Timers.SetTimer(FireHandle, this, &AMyActor::OnFire, 0.1f, Params);
FTimerHandle NextTick = Timers.SetTimerForNextTick(this, &AMyActor::OnFire);
Timers.PauseTimer(FireHandle); Timers.UnPauseTimer(FireHandle);
const float Remaining = Timers.GetTimerRemaining(FireHandle); // -1 if not found
Timers.ClearTimer(FireHandle); // invalidates the handle
Timers.ClearAllTimersForObject(this); // clears timers bound to this object; CreateLambda/TFunction timers need ClearTimer(Handle)FTimerDelegate is TDelegate<void(), FNotThreadSafeNotCheckedDelegateUserPolicy> (TimerManager.h:23); create it with CreateUObject, CreateWeakLambda, CreateLambda, CreateSP, CreateStatic (Delegates/DelegateSignatureImpl.inl). SetTimer overloads take a method pointer, FTimerDelegate, FTimerDynamicDelegate, TFunction<void(void)>&&, or no callback (handle-only countdown). Blueprint-facing: UKismetSystemLibrary::K2_SetTimer(UObject* Object, FString FunctionName, float Time, bool bLooping, bool bMaxOncePerFrame = false, float InitialStartDelay = 0.f, float InitialStartDelayVariance = 0.f) and K2_ClearAndInvalidateTimerHandle(const UObject* WorldContextObject, UPARAM(ref) FTimerHandle& Handle) (Kismet/KismetSystemLibrary.h:902,832). Timer examples: threading-patterns.md.
UPROPERTY read or write, UFUNCTION call, GetWorld(), spawning, destroying, component changes, delegates on UObjects, timers, tickers. Guard entry points with check(IsInGameThread()).UObject* or this into deferred work. Capture TWeakObjectPtr<T> (UObject/WeakObjectPtrTemplates.h:25) and resolve with Get() on the game thread, or build delegates with CreateWeakLambda.FGCScopeGuard (UObject/GarbageCollection.h:117) blocks GC for a scope; use it only for short read-only access from a worker, never around blocking waits.ENQUEUE_RENDER_COMMAND(MyCommand)([Data](FRHICommandListImmediate& RHICmdList) { UploadOnRenderThread(RHICmdList, Data); }); (RenderCore/Public/RenderingThread.h:1087, module RenderCore); FlushRenderingCommands() from the game thread drains it. Check with IsInRenderingThread().TSharedPtr; the refcount is atomic, the payload is not.TMpscQueue, std::atomic, double-buffering, or one FPipe per resource.Full patterns, lock ordering, double buffering, FScopedSlowTask and sanitizer notes: thread-safety-guide.md. Async asset loading (FStreamableManager::RequestAsyncLoad) belongs to ue-data-assets-tables.
| Do not emit | Use in 5.8 | Source |
|---|---|---|
FTicker::GetCoreTicker() | FTSTicker::GetCoreTicker() | FTicker is absent from the 5.8 headers; only FTSTicker exists (Containers/Ticker.h:26) |
FThreadSafeCounter | std::atomic<int32> | header comment "DEPRECATED. Please use std::atomic<int32>" (HAL/ThreadSafeCounter.h:9) |
FThreadSafeBool | std::atomic<bool> | header comment "DEPRECATED" (HAL/ThreadSafeBool.h:9) |
TAtomic<T> | std::atomic<T> | "planned for deprecation" (Templates/Atomic.h:13, :528); no UE_DEPRECATED macro yet |
FExternalMutex | TIntrusiveMutex<Params> (Async/IntrusiveMutex.h:60) | UE_DEPRECATED(5.7) in Async/ExternalMutex.h:73 |
TExternalMutex<Params> | TIntrusiveMutex<Params> | UE_DEPRECATED(5.8) in Async/ExternalMutex.h:23 |
FPlatformProcess::CreateSynchEvent(...) | GetSynchEventFromPool / ReturnSynchEventToPool, or FEventRef | UE_DEPRECATED(5.0) in GenericPlatform/GenericPlatformProcess.h:776 |
TQueue<T, EQueueMode::Mpsc> / EQueueMode::Spsc | TMpscQueue<T> / TSpscQueue<T> | "planned for deprecation" (Containers/Queue.h:11) |
ParallelFor(Num, Body, bool bForceSingleThread, bool bPumpRenderingThread) | ParallelFor(Num, Body, EParallelForFlags) | bool overload kept at Async/ParallelFor.h:481; flags form :526 is the documented one |
AsyncPool(GThreadPool, ...) | AsyncPool(*GThreadPool, ...) | parameter is FQueuedThreadPool& (Async/Async.h:407); the pointer form does not compile |
TSharedPtr<T, ESPMode::NotThreadSafe> "because the default is not thread-safe" | TSharedPtr<T> — the default is ESPMode::ThreadSafe | Templates/SharedPointerFwd.h:25 |
TGraphTask<T> / FGraphEventRef for new work | UE::Tasks::Launch / FTask (recommendation, not a deprecation) | no UE_DEPRECATED in Async/TaskGraphInterfaces.h; FGraphEventRef still accepted as a UE::Tasks prerequisite (Tasks/Task.h:360) |
Touching a UObject from a worker: GC and other game-thread writes race with you.
// WRONG — Health is a UPROPERTY on this AMyActor
Async(EAsyncExecution::ThreadPool, [this]() { Health = ComputeHealth(); });
// RIGHT
Async(EAsyncExecution::ThreadPool, [Weak = TWeakObjectPtr<AMyActor>(this)]()
{
const float NewHealth = ComputeHealth();
AsyncTask(ENamedThreads::GameThread, [Weak, NewHealth]() { if (AMyActor* A = Weak.Get()) { A->ApplyResult(NewHealth); } });
});Blocking the game thread right after launching: Task.GetResult() or Future.Get() on the next line turns async into sync. Poll IsCompleted()/IsReady() in Tick, chain with Prerequisites, or hop back with AsyncTask(ENamedThreads::GameThread, Lambda).
Private members in an FNonAbandonableTask: friend class FAsyncTask<T> covers the constructor (it runs inside FAsyncTask), but not GetTask().Result read from your code. Make the result members public.
Nested FRWLock acquisition: FRWLock is not recursive; a read lock inside a read lock (or a write inside a read) deadlocks. Acquire once per call path or switch to FCriticalSection.
Shared mutable state inside ParallelFor:
TArray<int32> Data;
// WRONG
int32 Total = 0; ParallelFor(Data.Num(), [&](int32 i) { Total += Data[i]; });
// RIGHT
std::atomic<int32> Total{ 0 }; ParallelFor(Data.Num(), [&](int32 i) { Total.fetch_add(Data[i], std::memory_order_relaxed); });Destroying an FPipe or FRunnable owner with work in flight: call Pipe.WaitUntilEmpty() and Thread->Kill(true) before the destructor body runs; ~FPipe() asserts !HasWork().
Blocking inside FRunnable::Stop(): Stop() runs on the caller's thread while Run() is still executing; only set an atomic flag or trigger an event, then let Kill(true) wait.
Raw-delegate timers on a dying actor: FTimerDelegate::CreateLambda([this]{}) keeps calling after EndPlay; use CreateWeakLambda/CreateUObject and ClearAllTimersForObject(this).
ue-cpp-foundations — TSharedPtr/TWeakObjectPtr/TStrongObjectPtr semantics, GC lifetime, subsystems tableue-data-assets-tables — FStreamableManager, UAssetManager, async asset loading and soft referencesue-testing-debugging — Unreal Insights task and thread traces, stat commands, logging, automation tests for async codeue-procedural-generation — long-running generation on FAsyncTask/UE::Tasks, ProceduralMeshComponent hand-off to the game threadue-mass-entity — ParallelForEachEntityChunk, EParallelExecutionFlags, processor threading rulesue-networking-replication — RPC and replication callbacks always run on the game threadue-blueprint-cpp-interop — exposing C++ to Blueprint: UFUNCTION/UPROPERTY meta keys, latent actions and async nodesue-niagara-effects — Niagara systems, user parameters, data interfaces and data channelsue-serialization-savegames — USaveGame, FArchive, actor snapshots and config persistence© quodsoler, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
SKILL.md and 2 other files (references) in skills/ue-async-threading of quodsoler/unreal-engine-skills.
Open the folder on GitHubat commit f3742d7
Unreal Engine Async and Threading next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Unreal Engine Async and Threading this skillquodsoler/unreal-engine-skills | 360 | — | ~8k | Automated safety check: Pass | MIT | |
| Unreal Engine MCPEpicGames/unreal-engine-skills-for-claude-code-plugin | 332 | — | ~1.8k | Automated safety check: Pass | MIT | |
| Unreal Live CodingItalink/UnrealClientProtocol | 127 | — | ~1k | Automated safety check: Pass | MIT | |
| Unreal Cpp Gameplayukanwat/overtime | 387 | 1 repos | ~1.5k | Automated safety check: Pass | Apache-2.0 | |
| Unreal Enhanced Inputukanwat/overtime | 387 | 1 repos | ~1.5k | Automated safety check: Pass | Apache-2.0 | |
| Unreal Niagaraukanwat/overtime | 387 | 1 repos | ~1.5k | Automated safety check: Pass | Apache-2.0 |
EpicGames/unreal-engine-skills-for-claude-code-plugin
Drives a live Unreal Editor through the unreal-mcp server: spawn actors, edit Blueprints, materials and sequences, change properties and trigger Live Coding.
Italink/UnrealClientProtocol
Trigger Live Coding compilation via UCP. An agent skill from Italink/UnrealClientProtocol.
ukanwat/overtime
Write Unreal Engine 5 C++ gameplay code: the UCLASS/UPROPERTY/UFUNCTION reflection macros, the Gameplay Framework (GameMode, Pawn, Character, PlayerController, Actor components), and the module…
ukanwat/overtime
Set up player input in Unreal Engine 5 with Enhanced Input: Input Actions, Input Mapping Contexts, modifiers and triggers, adding the mapping context, and binding actions by ETriggerEvent.
ukanwat/overtime
Create and control VFX in Unreal Engine 5 with Niagara: systems and emitters, modules and the spawn/update stages, exposed User parameters, and spawning or driving effects from Blueprints or C++.
scenario-labs/skills
A skill your agent uses when building or debugging UE5 gameplay: a character with double jump or dash, abilities with cooldowns, enemy AI that patrols, chases and attacks, a health bar or HUD…
quodsoler/unreal-engine-skills
Unreal Engine C++ guidance for AActor and UActorComponent: lifecycle hooks, spawning, component creation and attachment, ticking, interfaces and composition.
quodsoler/unreal-engine-skills
Guides Unreal Engine C++ AI work across AI controllers, behavior trees, blackboards, perception, navmesh pathfinding, EQS and Smart Objects.
quodsoler/unreal-engine-skills
Reference for writing and debugging Unreal Engine 5.8 animation C++: AnimInstance and proxy, montages, notifies, curves, blend spaces, state machines, linked layers and root motion.
quodsoler/unreal-engine-skills
A skill your agent uses when playing, mixing or debugging sound in Unreal Engine — one-shot SFX, looping ambience, music, dialogue or procedural audio.
quodsoler/unreal-engine-skills
Reference for exposing Unreal Engine C++ to Blueprint and calling Blueprint from C++, covering UFUNCTION and UPROPERTY specifiers, latent and async nodes, and interfaces.
quodsoler/unreal-engine-skills
Reference for writing and debugging UCharacterMovementComponent code in Unreal Engine C++: custom movement modes, floor detection, root motion, prediction and gravity.
Works with
Categories
Picks and applies the right Unreal Engine C++ concurrency API, from UE::Tasks and FPipe to ParallelFor, FRunnable, locks and timers, with game-thread safety rules. 8 with APIs checked against those headers. It spans the UE::Tasks system and FPipe, Async with TFuture and TPromise, thread-pool FAsyncTask work, ParallelFor loops, dedicated FRunnable threads, locks and lock-free queues, and tickers and timers.
Unreal Engine Async and Threading fits situations like: moving expensive work off the game thread in Unreal C++; dispatching results from a background task back to the game thread; running a data-parallel loop with ParallelFor; choosing between locks, queues and pipes for shared state.
Run `npx skills add quodsoler/unreal-engine-skills --skill ue-async-threading -a claude-code`. Or copy the skill folder (skills/ue-async-threading in quodsoler/unreal-engine-skills) into .claude/skills/ue-async-threading in your project. Claude Code loads it when a task matches its description.
Run `npx skills add quodsoler/unreal-engine-skills --skill ue-async-threading -a codex`. Or copy the skill folder (skills/ue-async-threading in quodsoler/unreal-engine-skills) into .agents/skills/ue-async-threading in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add quodsoler/unreal-engine-skills --skill ue-async-threading -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/ue-async-threading, .gemini/skills/ue-async-threading, .github/skills/ue-async-threading and .opencode/skills/ue-async-threading in your project.
SKILL.md names no scripts, command-line tools or credentials: Unreal Engine Async and Threading is instructions for the agent only. Our summary lists: An Unreal Engine C++ project (the skill targets UE 5.8).
SKILL.md contains no URLs. Any network use would come from the scripts or tools the agent runs. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.
Unreal Engine Async and Threading is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
About 8k tokens (SKILL.md is roughly 32k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full. Its references folder adds about 8.4k tokens, read only when the agent opens those files.
Skills that share tags, products or a category with Unreal Engine Async and Threading: Unreal Engine MCP (EpicGames/unreal-engine-skills-for-claude-code-plugin, 332 stars), Unreal Live Coding (Italink/UnrealClientProtocol, 127 stars), Unreal Cpp Gameplay (ukanwat/overtime, 387 stars) and Unreal Enhanced Input (ukanwat/overtime, 387 stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
quodsoler (a GitHub user) maintains it in quodsoler/unreal-engine-skills, which has 360 GitHub stars. The repository holds 31 skills in this directory. The repository was last updated on September 28, 2026.
Source: quodsoler/unreal-engine-skills on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.