Modspot

HomeBrowseBlog
Search
Custom SpawnPoint entity
By Aeltothin

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

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: image

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 the Node properties where default values for the editable variables can be set.

image

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

image

Use the Vertex edit tool to tweak the trigger area of the entity, you can also add edges with CTRL+Click.

Defining editable variables

image

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:

image

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));
}