-
Notifications
You must be signed in to change notification settings - Fork 9
Quirks
Here are some implementation details and inherent quirks with mapping TypeScript to Blueprint that you should probably be aware of if you're going to develop with TypeScript for Unreal.
- Self parameters
- Immutable types
- Reference equality
- Extension methods
- Debugging proxies
- Module-scope state
- "Why is [some function] not available?"
- Web/Node.js APIs
- Reacting to a hot-reload
In case you get tired of constantly wiring the Self
node to your TSU functions, you can enable the Use Self Parameter
setting in Project Settings -> Plugins -> TypeScript for Unreal
. Whatever name you put into Self Parameter Name
will have the DefaultToSelf
metadata assigned to it, which means wiring that parameter will become optional.
Note that you can not use the parameter name self
or this
, because those are reserved by Blueprint and TypeScript respectively. Also note that changing these settings mean you'll have to recompile all your existing TSU blueprints.
Certain types in Blueprint, such as Vector
, Rotator
and Transform
are marked as immutable. You'll see this represented in their typings as having readonly
properties. This means that they can never be modified.
In Blueprint itself this never really becomes an issue, since you always "break" and "make" those types if you want to modify a specific property on them, meaning you always create new instances instead of modifying existing ones.
In TypeScript however this leads to something as simple as player.position.x = 5
becoming disallowed, and instead you have to do player.position = player.position.withX(5);
.
Because of how TSU is implemented, equality between references can be potentially dangerous. So something like...
export function test(player: BP_Player) {
// Don't do this
if (player.mesh === player.mesh) {
console.log('Success!');
}
}
... might seem harmless (and nonsensical), but can in some cases fail. This is because .mesh
is a property, and will inside its getter create a new object (sometimes), meaning the equality check can fail. What you should do instead is rely on any equality method that might be available, like...
export function test(player: BP_Player) {
// Do this instead
if (player.mesh.equals(player.mesh)) {
console.log('Success!');
}
}
Also note that because of this, using references as keys for Map
is inadvisable, as is using Array
methods like indexOf
or includes
when dealing with arrays of references.
One of the fundamental features of TSU is what's commonly referred to as "extension methods" in other languages. They allow you to extend the API of an existing class without directly modifying it. TSU implements this by assuming that any method inside a UBlueprintFunctionLibrary
is an extension to the type of its first argument.
As an example, let's take the Vector2D
type. Without any extension methods at all, the typings for it would look something like:
declare class Vector2D {
readonly x: number;
readonly y: number;
}
... and then you would have to use the operations found in KismetMathLibrary
:
export function translate(origin: Vector2D, offset: Vector2D) {
return KismetMathLibrary.addVector2D(origin, offset);
}
This can get quite verbose after a while, and doesn't look very nice when you start chaining operations. TSU therefore takes all the library methods that has Vector2D
as its first argument and adds them to the Vector2D
type, like so:
declare class Vector2D {
readonly x: number;
readonly y: number;
/** Returns addition of Vector A and Vector B (A + B) */
add(b: Vector2D): Vector2D;
// Plus a bunch more extension methods...
}
As you can see the first argument is gone, and the comment doesn't perfectly reflect the parameters anymore. Also, the name of the method has been trimmed down to remove redundancy. Now we're now able to do this instead:
export function translate(origin: Vector2D, offset: Vector2D) {
return origin.add(offset);
}
While debugging you might encounter certain Blueprint properties where its type is Proxy
. This is due to implementation details with regards to struct and array properties. At runtime proxies are completely transparent, but when debugging you will (unfortunately) see its real Proxy
type.
If you're already familiar with proxies you might be inclined to look at its [[Target]]
in hopes of seeing the actual underlying property, but you should instead look for the actualObject
or actualArray
property inside the [[Handler]]
of the proxy.
Example of a struct property (with the desired property marked in red):
Example of an array property (with the desired property marked in red):
You might be tempted to store state in the module-scope of a file, like...
let count = 0;
export function foo(thing: BP_Thing) {
console.log(count++);
}
... but keep in mind that the context in which things are executed might get reset at any moment (due to hot-reloading or other factors). Exported functions in TSU are assumed to be "pure", in the sense that they don't rely on any persistent state within the JavaScript context itself. Instead, do something like...
export function foo(thing: BP_Thing) {
console.log(thing.count++);
}
Certain nodes/functions in Blueprint are bespoke, and not available in the reflection API that TSU relies on. This include nodes like Add Static Mesh Component
, Timeline
and Delay
.
The workaround for components/timelines is to do something like...
const component = new TimelineComponent(parent);
component.registerComponent();
// ...
parent.addOwnedComponent(component);
In the case of Delay
, you can instead use the global function setTimeout
. Keep in mind though that Delay
in Blueprint will simply stop the execution if there is an ongoing delay, whereas setTimeout
will create an additional delay instead.
Also remember that you can always wrap missing functionality inside a Blueprint function and call that from TypeScript instead.
If there's anything you feel is missing or is inconvenient then please feel free to open an issue about it.
TSU only implements a minimal subset of the APIs that JavaScript developers might be familiar with, like console.log
and setTimeout
. As such, stuff like WebSocket or other web APIs are not available. The same applies to any Node.js APIs you might usually reach for, like fs
, path
or process
.
If for whatever reason you would like to perform something at the time of a hot-reload, you can have your Blueprint class implement the TsuHotReloadListenerInterface
under Edit Class Settings
. This will give you access to the Post Hot Reload
event in your Blueprint graph.