Witcher 3 REDkit: creating a custom SpawnPoint entity
One of the strong points of Witcher 3's engine is the ability to write custom scripts for our own entities. This means that in scenarios where the pre-made components like the encounters are certainly functional but a bit tough to use, we can easily write a few lines of code to create our own spawn-point building block in order to make the creation of encounters a bit easier.
This article covers three core concepts while showcasing how to build a custom spawn point that spawns creatures when the players enters a specific area:
- The entity templates and the ability to create new ones with total control of what's inside them
- The component based nature of the entities with inner logic assigned to each component
- Writing custom classes that rely on components and that are meant for entity templates
Summary
- Summary
- Writing the code: a custom class
- Creating the entity template
- Using our new entity
- Going further
Writing the code: a custom class
The complete code is shared at the end of the article, this section showcases snippets of code to stay focused on what matters.
For a class to be assigned to an entity, it must extend CEntity
or CGameplayEntity
if it's dynamic:
statemachine class WMH_AreaSpawnPoint extends CGameplayEntity {}
Note that this class uses the
WMH_
prefix, that's because i'm using it in my Wild Monster Hunt mod. **If you copy the snippets of code from this article, make sure to change the prefix or else it will create a conflict.
The class is marked as a statemachine
as later-on we will need to perform asynchronous tasks
to load the templates without causing framerate drops. Only statemachines can start latent function calls.
For this reason we will add two states, an empty Idle
that's the default when nothing is performed and a Spawning
one while loading the templates and spawning the creatures. Once everything is done the statemachine goes back to the Idle state.
state Idle in WMH_AreaSpawnPoint {}
state Spawning in WMH_AreaSpawnPoint {
// the content of this state will appear later in the article
}
Components & events
Entities can be given extra capabilities with components. Each type of component does a very specific thing, and most of them are already available and cannot be edited by witcherscript as they're written in the C++ side of the engine.
As we want the spawn point to react to the player approaching, we will use the Area
type of component and more specifically the CTriggerAreaComponent
to get informed of any NPC entering the area designated by the component.
Once an entity has a CTriggerAreaComponent
it can listen to the two following events:
event OnAreaEnter(area: CTriggerAreaComponent, activator: CComponent) {}
event OnAreaExit(area: CTriggerAreaComponent, activator: CComponent) {}
In the case of our spawn point we only need to be informed when the player approaches, so we will only use the former one. It is also important to filter out unwanted calls to this event as any NPC can trigger it, for that we can use activator.GetEntity()
and compare it to thePlayer
. Events can be cancelled by returning false
even if the signature does explicitly tell that events can return booleans:
event OnAreaEnter(area: CTriggerAreaComponent, activator: CComponent) {
if (activator.GetEntity() != thePlayer) {
return false;
}
// ... do our things normally from here
}
We also use the OnSpawned
event that triggers when the entity is spawned in the world to initialize the statemachine:
event OnSpawned(spawnData: SEntitySpawnData) {
// calling `super` is important as spawnData contains information
// for when the entity is reloaded and such
super.OnSpawned(spawnData);
this.GotoState('Idle');
}
Editable variables
As each spawn point differs on what type of creature and the amount it spawns, and we do not want to create a different template for each type we'll make it so the entity offers editable
variables.
The editable variables are parts of the class that can be edited on a per-entity basis once they're placed in the world. That way each spawn point can be assigned a different type of creature with a different respawn delay etc...
The hint
keyword can then be used to add descriptions to these fields.
editable var template_paths: array<string>;
hint template_paths = "paths to the w2ent this point can spawn.";
editable var spawn_count_max: int;
default spawn_count_max = 1;
editable var spawn_count_min: int;
default spawn_count_min = 1;
editable var respawn_cooldown: int;
hint respawn_cooldown = "time in seconds between two spawns created by this spawn point.";
editable var entity_tags: array<name>;
hint entity_tags = "tags that should be given to the spawned entities.";
Full code
Now that the core parts of our spawnpoint's class are covered, here is the full code for it:
statemachine class WMH_AreaSpawnPoint extends CGameplayEntity {
editable var template_paths: array<string>;
hint template_paths = "paths to the w2ent this point can spawn.";
editable var spawn_count_max: int;
default spawn_count_max = 1;
editable var spawn_count_min: int;
default spawn_count_min = 1;
editable var respawn_cooldown: int;
hint respawn_cooldown = "time in seconds between two spawns created by this spawn point.";
default respawn_cooldown = 600;
editable var entity_tags: array<name>;
hint entity_tags = "tags that should be given to the spawned entities.";
/// store the timestamp of the last time, when compared to the current timestamp
/// and with this.respawn_cooldown this will be used to detect when a respawn is
/// allowed.
private var spawn_timestamp: float;
private var entities: array<CNewNPC>;
event OnSpawned(spawnData: SEntitySpawnData) {
super.OnSpawned(spawnData);
this.GotoState('Idle');
}
event OnAreaEnter(area: CTriggerAreaComponent, activator: CComponent) {
if (activator.GetEntity() != thePlayer) {
return false;
}
if (this.canRespawn() && this.GetCurrentStateName() != 'Spawning') {
this.spawn_timestamp = theGame.GetEngineTimeAsSeconds();
this.GotoState('Spawning');
}
}
private function canRespawn(): bool {
var now: float = theGame.GetEngineTimeAsSeconds();
var delta: float = now - this.spawn_timestamp;
return this.spawn_timestamp <= 0 || delta >= this.respawn_cooldown;
}
}
state Idle in WMH_AreaSpawnPoint {}
state Spawning in WMH_AreaSpawnPoint {
event OnEnterState(previous_state_name: name) {
super.OnEnterState(previous_state_name);
this.Spawning_main();
}
entry function Spawning_main() {
var resource: CEntityTemplate;
var template_path: string;
var entity: CEntity;
var count: int;
count = this.calculateSpawnCount();
if (count <= 0) {
parent.GotoState('Idle');
return;
}
while (count > 0) {
count -= 1;
// there is an obvious flaw here, if a resource for a template_path was
// already loaded in a previous iteration this will load it again when
// it could have been cached.
//
// Hopefully the engine does it for us under the hood, otherwise we'll
// have to write a bit of code to handle that if it becomes problematic
template_path = this.getRandomTemplate();
resource = (CEntityTemplate)LoadResourceAsync(template_path, true);
entity = theGame.CreateEntity(
resource,
// position:
parent.GetWorldPosition() + VecRingRand(0.0, 5.0),
// rotation:
VecToRotation(VecRingRand(1, 2)),,,,
// persistance, whether entities stays between reloads
PM_DontPersist,
parent.entity_tags
);
}
parent.GotoState('Idle');
}
private function calculateSpawnCount(): int {
if (parent.spawn_count_max == parent.spawn_count_min) {
return parent.spawn_count_max;
}
return RandRange(parent.spawn_count_max, parent.spawn_count_min);
}
private function getRandomTemplate(): string {
var index: int = RandRange(parent.template_paths.Size());
return parent.template_paths[index];
}
}
Creating the entity template
Now that the code is written it is time to set up the entity template, also known as .w2ent
files. Creating an empty template is as easy as right-clicking the Asset Browser and using the Create / Entity template
option:
This will leave you with an empty .w2ent
, opening it will give you a window with
two panels:
- left panel: shows a preview of the entity once it's placed in the world. This preview can be used to move around the meshes or components that make the entity, their position is relative to the center of the entity and will all move together once in the world. However components can still be moved around in the world which we'll happily use later-on so each placed version of that entity will have a customized trigger area with specific position and size.
- right panel, which is split in two parts:
- top right: lists the components or meshes in the template
- bottom right, and specifically the
Template properties
tab where a class can be assigned to the template and theNode properties
where default values for theeditable
variables can be set.
Adding components
To add a component right-click the grey background in the top right panel and use Select Component
. As you can see from the image above, two components were added to the entity:
CTriggerAreaComponent
: as specified earlier in the article this entity will come with its own trigger area component so it can react to the player approaching, allowing it to spawn the enemies when it happens.CSpawnPointComponent
: which isn't used much in the version of the entity described in this article but it makes it clearer there is a spawn point placed in the world.
Once a component is added you can select it in the top right panel and then move it around in the preview left panel. In my case i've made it so the trigger area component is around 1 meter large around the center and also a bit under the ground to properly detect the player walking on the ground. It doesn't matter too much though because the area is meant to be tweaked on a per-case basis after it's placed in the world.
Assigning our custom class to the template
In the bottom right panel, in the Template properties
tab the entity's class can be changed to the custom class we wrote by clicking the green arrow next to the field.
Once it's assigned clicking the grey background of the top right panel and then going in the Node properties
tab of the bottom right panel allows us to override the default values of the editable
fields we added to our class. This is particularly useful to create specialized variants of the template, for example having copies of that template with different default values each. Refer to Specialized templates: pre-filled templates for more information.
Using our new entity
Make sure to save the template before closing the template editor
The entity is now ready to be used, create a LT_AutoStatic & LBT_Gameplay layer and place a version of the template in it. Then use CTRL+ALT+F to open the REDkit settings and in the Editor options
/ Debug
make sure the following options are all enabled:
- Visual Debug
- Waypoints: to see the spawn point components
- Sprites: to move the corners of the trigger area
- Areas: to see the area edges
Tweaking the entity's area
Use the Vertex edit
tool to tweak the trigger area of the entity, you can also add edges with CTRL+Click.
Defining editable variables
The editable
variables in our class are visible in the entity properties, allowing us to assign specific values for each encounter we create.
Going further
Specialized templates: pre-filled variables
The Node properties
of the template can be given default values unique to the template. This means that we can copy the template and assign different values for various scenarios to save our time. For example the main variant of the template adds a tag to the entity_tags
array so that each sub-variant already has the value filled in. Then it is also possible to create templates for each monster species like the drowner one showed here:
That way there is no need to refill the array of template paths every time a drowner encounter has to be created, using that template instead of the original one will work exactly as expected.
Improved spawning behaviour
The class' spawning logic is very basic, if the respawn cooldown allows it then it spawns a random amount of enemies based on the editable variables. This works fine for simple encounters but for more elaborate ones it might be a good idea to create a sub-class or simply modify the class itself with more options. This segment will show a few ideas on how it can be extended and why.
Keeping track of living entities
The entities obtained by the theGame.CreateEntity()
call is currently not used anywhere, storing it in a var entities: array<CEntity>;
could allow the function canRespawn(): bool
to also check if there is still any entity left in order to cancel the respawn of new entities.
Looping through the array and using entity.IsAlive()
should be enough, but also remember that a killed entity may be replaced by a loot bag, causing the CEntity
to become NULL
so you may also want to check whether the entity is valid before calling IsAlive on it.
Persistent respawn timer
The current logic for the respawn cooldown relies on GetEngineTimeAsSeconds
, which means that closing the game and opening it again will reset the timer. While in theory this is a fine compromise for how simple the code becomes it could allow for farming (albeit boring) methods. A good solution would be to use the Facts DB as they offer a "valid for" parameter that persists through reloads:
private function canRespawn(): bool {
return FactsQuerySum(this.encounterFact()) <= 0;
}
protected function addRespawnCooldownFact() {
FactsSet(this.encounterFact(), 1, this.respawn_cooldown);
}
// returns a fact key that's unique to this class and changes based on
// the entity's position in the world
private function encounterFact(): string {
var position: Vector = this.GetWorldPosition();
return "WMH_AreaSpawnPoint_"
+ IntToString(FloorF(position.X))
+ "-"
+ IntToString(FloorF(position.Y))
+ "-"
+ IntToString(FloorF(position.Z));
}