Witcher 3 REDkit: creating a custom SpawnPoint entity
One of the strong points of Witcher 3's engine is the abilty 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 our own encounters a bit easier to setup.
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
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];
}
}