In the game, we use an "event keys" system. An event key is like a signal or trigger that the game sends out when something specific happens (like when the player gets an item or reaches a milestone).
The p_game.gd script is where events are handled. Inside this script, there’s a function that listens for these event keys.
The function takes a event_key_id as a parameter. This ID represents the event key the game is waiting for.
When the player receives the event key (from anywhere in the game), the function with that event_key_id is triggered, and the game executes whatever code is connected to that event key.
So, if you want to create a mod or add new content, you can trigger these event keys to make things happen in the game, like unlocking new features, changing the environment, or starting a special event.
Files & folders needed:
assets/scripts/p_game.gd
So basically event key can be received from, scripts, dialogues, items using, trigger zones. So you create a event key ID in assets/scripts/p_game.gd file, in the function called:
func on_event_key_add(key): # key -> is string ID of event
there you'll see a match statement, that trying to match key. So if game will match your event key ID, it will do something inside the function. Basically this on_event_key_add is callback and calls at the end of :
func AddEventKey(key)
function from GameManager. You can manipulate with events keys in scripts by:
GameManager.gd
func AddEventKey(key : String) # add new e.key
func DeleteEventKey(key : String) # delete e.key
func DontHasEventKey(key : String) # return true if no e.key
func HasEventKey(key : String) # return true if has e.key
So let's create event key where we can add NPC to the level. Open assets/scripts/p_game.gd. Go to on_event_key_add function and create new event key ID. Let's name it my_event_key.
Unfortunately I didn't create simple function for NPC creating, so we can create it together! Look into source code of the game at SOURCE_PROJECT/scripts/GameManager.gd and find there function:
GameManager.gd
func load_level(id : String,silent = false):
This function load json levels from files, but there is NPC creation code, that we can rewrite into separate function. Original NPC spawn code looks like:
GameManager.gd
var lvl_npc : NPC = preload("res://engine_objects/npc.tscn").instantiate()
await get_tree().process_frame
lvl_npc.name = obj["name"] if "name" in obj else "npc_"+str(lvl_npc.get_instance_id())
lvl_npc.position = Vector3(obj["position"][0],obj["position"][1],obj["position"][2])
lvl_npc.rotation_degrees = Vector3(obj["rotation"][0],obj["rotation"][1],obj["rotation"][2])
if "scale" in obj:
lvl_npc.scale = Vector3(obj["scale"][0],obj["scale"][1],obj["scale"][2]) if typeof(obj["scale"]) == TYPE_ARRAY else Vector3(obj["scale"],obj["scale"],obj["scale"])
else:
lvl_npc.scale = Vector3(0.8,0.8,0.8)
lvl_npc.keys = obj
lvl_npc.profile = npc_json[obj["profile"]]
lvl_npc.id = obj["profile"]
lvl_npc.PlayAnim("idle")
if "lit" in obj and obj["lit"]:
var orig_mat_0 : StandardMaterial3D = lvl_npc.npc_model.get_surface_override_material(0)
var orig_mat_1 : StandardMaterial3D = lvl_npc.npc_model.get_surface_override_material(1)
orig_mat_0.shading_mode = StandardMaterial3D.SHADING_MODE_UNSHADED
orig_mat_1.shading_mode = StandardMaterial3D.SHADING_MODE_UNSHADED
lvl_npc.npc_model.set_surface_override_material(0,orig_mat_0)
lvl_npc.npc_model.set_surface_override_material(1,orig_mat_1)
level_node.add_child(lvl_npc)
add_screen_notifier(lvl_npc)
editor_level_objects.append(lvl_npc)
Lets create our new function in separate *.gd file near p_game.gd script. Call new file an you want, for now it will be "my_mod_functions.gd" with this code:
my_mod_functions.gd
extends Node
func SpawnNPC(object_id : String = "", profile_id : String = "",position : Vector3 = Vector3.ZERO, rotation : Vector3 = Vector3.ZERO):
# create variable that hold reference to newly created NPC object
var new_npc : NPC = preload("res://engine_objects/npc.tscn").instantiate()
# set npc level keys data
new_npc.keys = {
"id": "npc",
"profile": profile_id
}
# set npc object data
# read profile JSON from Game Manager npc_json preloaded config
new_npc.profile = GameManager.npc_json[profile_id]
new_npc.id = profile_id
# set object name (we can find NPC by this name)
new_npc.name = object_id if object_id != "" else "npc_"+str(new_npc.get_instance_id())
# wait one frame
await get_tree().process_frame
# add NPC object at the current level
GameManager.current_level.add_child(new_npc)
# play idle animation after spawn
new_npc.PlayAnim(new_npc.profile["idle_animation"])
# set npc position and rotation
new_npc.position = position
new_npc.scale = Vector3(0.8,0.8,0.8)
new_npc.rotation_degrees = rotation
# return NPC as object so we can reference it elsewhere
return new_npc
then go back to p_game.gd script to the on_event_key_add where is our new event key and call SpawnNPC(id,profile,pos,rot) function from my_mod_functions.gd:
p_game.gd
func on_event_key_add(key):
# event keys match for logic
match key:
"my_event_key":
var my_mod_functions = GameAPI.RunOutsideScript("my_mod_functions")
my_mod_functions.SpawnNPC("my_test_npc","village_trader_profile")
my_test_npc <- ID for NPC finding. Like get_node("my_test_npc")
village_trader_profile <- ID from assets/creatures/charactres.json
Then let's add event key at the start of new game. Go to function:
p_game.gd
func on_level_changed(level_id):
Add into "intro" level this code:
p_game.gd
"intro":
# will add event key
GM.AddEventKey("my_event_key")
Start new game and you wll see:
Lets get some coordinates, turn on debug fly and debug mode in assets/scripts/p_app.gd:
p_app.gd
func game_init():
# this already exist in the file, just set false to true
GameManager.GameProcess.player.DebugFly = true
# this doesn't exist in file, add it by yourself and set true
GameManager.GameProcess.player.debug_info = true
When we start new game, we will see new window at the right top corner:
Fly at needed position with WASD and rotate camera where you want to NPC to look.
Let's do it near helicopter.
Keep in mind, player's rotation a little opposite to NPC rotation data, at screenshot you see -174 Y coordinate, but for NPC it must be 0 if it's equal to player 180 degrees, if you put -174 or 174 Y coordinate to NPC rotation then it will stands with back to where player was looked. My bad.
Add inside your event key code position and rotation for SpawnNPC().