UnrealCLR doesn't depend on how the development environment is organized. Any IDE such as Visual Studio, Visual Code, or Rider, can be used to manage a project. The programmer has full freedom to set up the building pipeline in any desirable way just as for a regular .NET library.
After building and installing the plugin, use IDE or CLI tool to create a .NET class library project which targets net6.0
in any preferable location. Don't store source code in %Project%/Managed
folder of the engine's project, it's used exclusively for loading and packaging user assemblies by the plugin.
Add a reference to UnrealEngine.Framework.dll
assembly located in Source/Managed/Framework/bin/Release
folder.
Assuming you put your code in %Project%/MyDotNetCode
, your project file should look similar to this:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<OutputPath>../Managed/Build</OutputPath>
</PropertyGroup>
<ItemGroup>
<Reference Include="UnrealEngine.Framework">
<HintPath>%UnrealCLR%/Source/Managed/Framework/bin/Release/UnrealEngine.Framework.dll</HintPath>
</Reference>
</ItemGroup>
</Project>
Create a new or open a source code file in the .NET project and replace its content with the following code:
C#
using System;
using System.Drawing;
using UnrealEngine.Framework;
namespace Game {
public class Main { // Indicates the main entry point for automatic loading by the plugin
public static void OnWorldBegin() => Debug.AddOnScreenMessage(-1, 10.0f, Color.DeepPink, "Hello, Unreal Engine!");
public static void OnWorldPostBegin() => Debug.AddOnScreenMessage(-1, 10.0f, Color.DeepPink, "How's it going?");
public static void OnWorldEnd() => Debug.AddOnScreenMessage(-1, 10.0f, Color.DeepPink, "See you soon, Unreal Engine!");
public static void OnWorldPrePhysicsTick(float deltaTime) => Debug.AddOnScreenMessage(1, 10.0f, Color.DeepPink, "On pre physics tick invoked!");
public static void OnWorldDuringPhysicsTick(float deltaTime) => Debug.AddOnScreenMessage(2, 10.0f, Color.DeepPink, "On during physics tick invoked!");
public static void OnWorldPostPhysicsTick(float deltaTime) => Debug.AddOnScreenMessage(3, 10.0f, Color.DeepPink, "On post physics tick invoked!");
public static void OnWorldPostUpdateTick(float deltaTime) => Debug.AddOnScreenMessage(4, 10.0f, Color.DeepPink, "On post update tick invoked!");
}
}
F#
namespace Game
open System
open System.Drawing
open UnrealEngine.Framework
module Main = // Indicates the main entry point for automatic loading by the plugin
let OnWorldBegin() = Debug.AddOnScreenMessage(-1, 10.0f, Color.DeepPink, "Hello, Unreal Engine!")
let OnWorldPostBegin() = Debug.AddOnScreenMessage(-1, 10.0f, Color.DeepPink, "How's it going?")
let OnWorldEnd() = Debug.AddOnScreenMessage(-1, 10.0f, Color.DeepPink, "See you soon, Unreal Engine!")
let OnWorldPrePhysicsTick(deltaTime:float32) = Debug.AddOnScreenMessage(1, 10.0f, Color.DeepPink, "On pre physics tick invoked!")
let OnWorldDuringPhysicsTick(deltaTime:float32) = Debug.AddOnScreenMessage(2, 10.0f, Color.DeepPink, "On during physics tick invoked!")
let OnWorldPostPhysicsTick(deltaTime:float32) = Debug.AddOnScreenMessage(3, 10.0f, Color.DeepPink, "On post physics tick invoked!")
let OnWorldPostUpdateTick(deltaTime:float32) = Debug.AddOnScreenMessage(4, 10.0f, Color.DeepPink, "On post update tick invoked!")
All functions of the main entry point are optional, and it's not necessary to implement them for every tick group.
Publish a .NET assembly to %Project%/Managed
folder of the engine's project, and make sure that no other assemblies of other .NET projects are stored there.
Assemblies that no longer referenced and unused in the project will persist in %Project%/Managed
folder. Consider maintaining this folder through IDE or automation scripts.
Enter the play mode to execute managed code. Stop the play mode to unload assemblies from memory for further recompilation.
C#
using System;
using System.Drawing;
using UnrealEngine.Framework;
namespace Game {
public class System { // Custom class for loading functions from blueprints
public static void Function() => Debug.AddOnScreenMessage(-1, 10.0f, Color.DeepPink, "Blueprint function invoked!");
}
}
F#
namespace Game
open System
open System.Drawing
open UnrealEngine.Framework
module System = // Custom module for loading functions from blueprints
let Function() = Debug.AddOnScreenMessage(-1, 10.0f, Color.DeepPink, "Blueprint function invoked!")
To run a blueprint function, create a new or open an existing level of the engine. Open level blueprint by navigating to Blueprints -> Open Level Blueprint
and create a basic execution flow by right-clicking on the graph and selecting nodes from the .NET category:
Compile the blueprint and enter the play mode.
The plugin is transparently integrated into the packaging pipeline of the engine and ready for standalone distribution.
The framework provides world events that are executed by the engine in a predetermined order to drive logic and simulation. Event functions are automatically loaded by the plugin from Main
class for further execution.
OnWorldBegin()
Called after the world is initialized before the level script.
OnWorldPostBegin()
Called after the level script when default actors are spawned.
OnWorldEnd()
Called before deinitialization of the world after the level script.
OnWorldPrePhysicsTick(float deltaTime)
Called at the beginning of the frame.
OnWorldDuringPhysicsTick(float deltaTime)
Called when physics simulation has begun.
OnWorldPostPhysicsTick(float deltaTime)
Called when physics simulation is complete.
OnWorldPostUpdateTick(float deltaTime)
Called after cameras are updated.
The plugin allows organizing the code structure of the project in any preferable way. Any paradigms or patterns can be used to drive logic and simulation without any intermediate management between user code and the engine.
Object-Oriented Design
using System;
using System.Drawing;
using UnrealEngine.Framework;
namespace Game {
public class Main {
private static Entity[] entities = new Entity[32];
public static void OnWorldBegin() {
for (int i = 0; i < entities.Length; i++) {
entities[i] = new Entity(nameof(Entity) + i);
entities[i].OnBegin();
}
}
public static void OnWorldPrePhysicsTick(float deltaTime) {
for (int i = 0; i < entities.Length; i++) {
if (entities[i].CanTick)
entities[i].OnPrePhysicsTick(deltaTime);
}
}
}
public class Entity : Actor {
public Entity(string name = null, bool canTick = true) : base(name) {
CanTick = canTick;
}
public bool CanTick { get; set; }
public void OnBegin() => Debug.AddOnScreenMessage(-1, 1.0f, Color.LightSeaGreen, Name + " begin!");
public void OnPrePhysicsTick(float deltaTime) => Debug.AddOnScreenMessage(-1, 1.0f, Color.LightSteelBlue, Name + " tick!");
}
}
Data-Oriented Design
using System;
using System.Drawing;
using UnrealEngine.Framework;
namespace Game {
public class Main {
private static Actor[] entities = new Actor[32];
private static bool[] canTick = new bool[entities.Length];
public static void OnWorldBegin() {
for (int i = 0; i < entities.Length; i++) {
entities[i] = new Actor("Entity" + i);
canTick[i] = true;
OnEntityBegin(i);
}
}
public static void OnWorldPrePhysicsTick(float deltaTime) {
for (int i = 0; i < entities.Length; i++) {
if (canTick[i])
OnEntityPrePhysicsTick(i, deltaTime);
}
}
private static void OnEntityBegin(int id) => Debug.AddOnScreenMessage(-1, 1.0f, Color.LightSeaGreen, entities[id].Name + " begin!");
private static void OnEntityPrePhysicsTick(int id, float deltaTime) => Debug.AddOnScreenMessage(-1, 1.0f, Color.LightSteelBlue, entities[id].Name + " tick!");
}
}
See tests project for a basic implementation of various systems.
The plugin provides two blueprints to manage the execution. They can be used in any combinations with other nodes, data types, and C++ code to weave managed functionality with events. It's highly recommended to use creative approaches that extract information from blueprint classes instead of using plain strings for managed functions.
Find Managed Function
Attempts to find a managed function from loaded assemblies. This node performs a fast operation to retrieve function pointer from the cached dictionary once assemblies loaded after entering the play mode. Logs an error if the managed function was not found and sets Result
parameter to false
. Optional
checkbox indicates if the managed function is optional and the error should not be logged.
Execute Managed Function
Attempts to execute a managed function. This node performs a fast execution of a function pointer. Optionally allows passing an object reference of the engine to managed code with further conversion to an appropriate type.
Several options are available to pass data between the managed runtime and the engine.
Commands, functions, and events
The engine's reflection system allows to dynamically invoke commands, functions, and events of engine classes from managed code to pass data on-demand through custom arguments.
Blueprint event dispatcher:
blueprint.Invoke($"TestEvent \"{ message }\" { value }");
See Actor.Invoke(), ActorComponent.Invoke(), and AnimationInstance.Invoke() methods.
Blueprint variables
The engine provides a convenient way to manage variables/properties per actor/component basis that are accessible from the world outliner.
Instance-editable blueprint variables:
Exposed as properties and accessible from the world outliner:
See Actor, ActorComponent, and AnimationInstance classes for appropriate methods to get and set data.
Console variables
Data that should be globally accessible can be stored in console variables and modified from the editor's console.
See ConsoleVariable class for appropriate methods to get and set data.
The plugin is compatible with .NET tools and makes the engine's application instance visible as a regular .NET application for IDEs and external programs.