NPC routines

Started by wizard_wizzle (aka ZeldaHistorian), December 06, 2017, 04:41:41 AM

Previous topic - Next topic
Hey all,

I've been thinking recently about how to create more interesting NPC routines in Lua without it being too complicated. Right now I'm on the idea of passing routines as a table from the NPC script to another scripts that parses it and stores it to be executed on the regular. I'm a little fuzzy on the details though, so I thought I'd throw my thoughts here!

Code (lua) Select

local routine = { [08]="to_shop", [10]="to_house", [13]="to_mill" }
function entity:on_created()
  self:set_routine(self, routine)

Here is where I thought about encoding the routine as a nested table - with time (hours) in brackets and waypoint entity in quotes. Then I can parse that table out to get the individual parts of the routine.

Code (lua) Select
  for k,v in pairs(routine) do
    game:add_routine(k, npc:get_name(), v)

I think my next step would be to create a new table, or several tables, organized by hour so the game easily parse the correct table at the correct time (the add_routine routine I'm playing around with up there). If this table contains the entity name and waypoint entity name, then the movement should be able to be created pretty easily.

My main issue right now is the structure/format of the routine tables organized by time. I'm not an expert on Lua tables and I'm not sure how to make this without a bunch of if/then statements which would be best to avoid.

Anyway, I'd love to hear thoughts on the idea, and especially the execution thereof!

December 06, 2017, 05:13:04 AM #1 Last Edit: December 06, 2017, 05:17:49 AM by Satoh
Why not call the table as an array that's indexed by the hour?

as in,
Code (lua) Select

local current_routine = routine_table[current_hour] or current_routine
--If the routine at the current_hour is nil this will default back to previous value

If the current_hour variable is 8, that would call whatever routine is set to 8, in your example, "to_shop".
On hour 9, it might return nil, but because of the or, it will retain the value at 8. You can of course remove the 'or current_routine' and do something like
Code (lua) Select
if current_routine ~= nil then [ some code...] end

Or maybe I'm misunderstanding the question.

EDIT: to clarify a point
In lua, if you set any variable to "nil or [some value]" [some_value] will always be the new value of the variable, as nil is treated as a last resort value.
The same is true of var=value or nil. var will equal value, not nil.
Patience is difficult and rarely thanked, but always appreciated, and sorely missed when absent.

You might want to check out what I did for NPC movements in my Cythera sample quest. Specifically you should check out npc.Neoptolemus.lua. Local paths defines the routes from every "waypoint" on the map to every other "waypoint" by listing out the steps taken. My recollection is that I timed my NPC movements such that 1 step is taken per minute in-game (where what I'm calling a step is an 8 pixel movement).

The local locations defines the coordinates of each "waypoint". Note that locations are defined for 2 maps: The town map and the map for inside his house. Basically he gets up out of bed in the morning and walks over to his table, waits briefly then walks outside where he stands for the rest of the day, comes back inside at night waiting at his table again, then walks back to the bed and goes to sleep. I placed an NPC for him on both maps, and it is hidden off-screen during times when he is not present on the current map.

Finally the local schedule defines when he moves from waypoint to waypoint, with an entry for each map the NPC is present in. I specify times to the minute. So for example, he starts walking from his table to his door at 8:00, and it is 24 steps to his door. So the next event begins at 8:23. Events where the NPC is waiting stationary link only to a location waypoint; events where the NPC moves from one waypoint to another link to both a location and a path.

Also note that I defined some movements for the NPC Demodocus as well, but I ended up not using them in the quest since the NPC sprite I used for him in the ALTTP pack did not have movement sprites, and in the end I just made him stand stationary in the bar. My plan was to have him enter the town from the West, walk to the tavern, then exit the town to the East at the end of the day.

Thanks for the thoughts so far!

llamazing, that's one approach, but basically I'm trying to avoid outlining the per-step movement of every NPC. I'm also trying to move the bulk of the code out of the NPC script and have it handled by an outside routine. Basically as I start applying movement to ten or twenty NPCs in a town, I don't want to spend weeks outlining the movement. It also would be nice to have some randomness in their routines, which is why I'm relying on the "waypoint" approach.

Satoh, I'm certain I didn't explain myself well! I like the syntax that I presented for local routine - it's simple and is capable of outlining an NPC's entire day in one line of code - as long as it can be parsed down the line correctly. In my example, the time between 8 and 10 would be spend pathfinding (with some random movement toward the location thrown in, mostly because of Solarus's current limitations in the pathfinding movement) from the "to_shop" entity to the "to_home" entity.
My problem arises with how to translate this into something that can execute routines at a set time for potentially tens of NPCs. My initial thought was to "register" each NPC's routine when they're created on the map, then have a function that re-arranges this routine in order to place each NPC/waypoint into an hour table. Mostly I'm not sure how to define these tables without having potentially 24 of them...

you'll probably need more than 24, since each NPC will have a different daily schedule.

Also, are you aware that a table can have a function as a member just like any variable?

if you define table[8] = function [...] end

then calling for the value of table[8] will execute that function.

if you really wanted to make this be a universal AI module, you're looking at a lot of scripts. Don't get me wrong, it's a great idea, but it will take some effort.

For example, you'll have to develop your own sort of scripting shorthand language to handle pathfinding, which is a script to itself.
You'd have to write a pathfinding algorithm for go_home, that doesn't know where home or any of its waypoints are, (because they're unique to each NPC) but instead must be fed a table of waypoints from its calling NPC. That NPC would have a table named go_home, possibly inside a bigger table of things including other entries like go_work, etc.

an NPC's schedule would likely be a table as well, defined as you did in the first post with [8]="go_work" and so forth.

You'll want all of the actual handling to be done in one file, so you don't have to copy all of it to each NPC, so...
What you could do, is write a file that reads "go_" instructions and uses a specific function for all of them, looking up the appropriately named tables inside the calling NPC ( schedule_table["go_work"] ) to find the list of waypoints the NPC needs to be pushed toward.

similarly you'd have a function that exclusively handled "do_" functions, for things like playing animations and such without moving.

This script would be called with a require at the top of each NPC who uses the schedule system, and you'd have a timer that calls the scheduler function every x milliseconds (the duration of one game hour) or something similar.

Patience is difficult and rarely thanked, but always appreciated, and sorely missed when absent.

December 07, 2017, 03:02:14 AM #5 Last Edit: December 07, 2017, 03:03:51 AM by llamazing
I think there's still some insight that can be gained from my scripts. Really, the only key difference I'm seeing between my implementation and what you seek is to replace my hard-coded path movements with a script that does the path-finding dynamically.

Of course, that is by no means a trivial task. When I was doing the planning for how to implement my script, I was thinking it would be nice to be able to draw the paths for the NPCs to follow in the map editor. You could do the next best thing and place the waypoints on the map using invisible custom entities (for example, place a waypoint at every intersection along a path and at every relevant destination). Then you could make use of either a target movement or path finding movement instead of the path movement I used in my script.

This implementation still isn't a slam dunk because I'm assuming that you'd want to only have to specify the end destination and not all the intermediate waypoints on the route to get there, so you'd still need a script to find the ideal sequence of waypoints to traverse to get to the ultimate destination.

There are a few complications that you'll have to consider. One example is when the player enters a map, you'll need a script that will determine exactly where every NPC should be located on the map based on the time when the player entered the map. This can be tricky because the NPCs could be in the middle of their movements, so you will have to position the NPCs accordingly and then start a movement from their current location.

Another complication is the timing aspect. If an NPC takes too long getting to their destination, then the subsequent movement will either be delayed (a problem that can become compounded over time), or there will be discontinuities in the movement where the NPC instantly skips across a segment of the movement in order to catch up. In my case, having a precisely determined path helps mitigate this issue.

One thing you can do to help with this problem is to add slop by having the NPC wandering around randomly for a brief duration between movements. For example, if it takes 20 seconds for an NPC to move from point A to B, you could allocate 30 seconds before having the NPC move to point C, where the NPC wanders randomly in the vicinity of point B until the 30 seconds has elapsed. So then whether it takes 18 seconds or 24 seconds for the NPC to travel from A to B, it makes no difference because the NPC would be wondering randomly near point B for either 12 or 6 seconds according.

To go into more details about how I implemented my event tables...

I used a map based timer for my game clock ( timer starts on map:on_started() ). The advantage of linking the timer to the map is that the timer automatically gets paused whenever a dialog is displayed or the game is paused. The tricky part is transitioning the timer on changes of the map. What I did in on map:on_finished(), save the current time to the save data file and then kill the timer. Then when the next map loads, start a new timer from the time saved to the save data file. See game_clock.lua map_meta:register_event().

Quote from: wrightmat on December 06, 2017, 02:51:28 PM
My initial thought was to "register" each NPC's routine when they're created on the map, then have a function that re-arranges this routine in order to place each NPC/waypoint into an hour table.

That is basically the implementation that I used. I have a static table of events for each NPC in my data/events directory. When a map loads (map:on_started()) the relevant table entries corresponding to the current map for any NPCs present are extracted and assembled into a new events_list table with the event start times as keys. (see game_events.lua This is the table structure (keep in mind the table is generated dynamically, hard-coded here for clarity):
Code (lua) Select

local events_list = {
["8:00"] = {
{ --NPC_1
npc_id = "NPC Name #1",
location = {x=96, y=157, layer=0, facing=3},
path = {4,4,4,2,2,2,2,2}, --hard-coded path
target = "to_house", --you could do something like this instead, corresponding to a custom entity with this name
{ --NPC_2
npc_id = "NPC Name #2",
location = {x=160, y=237, layer=1, facing=1},
path = {1,2,3,4}, --hard-coded path
["8:23"] = {
{ --NPC_1
npc_id = "NPC Name #1",
location = {x=216, y=109, layer=1, facing=3},
path = {2,2,2,2,2}, --hard-coded path
["9:00"] = {
{ --NPC_2
npc_id = "NPC Name #2",
location = {x=72, y=61, layer=1, facing=3},
path = {6,6,8,8,2,2,4,4}, --hard-coded path

Then every in-game minute the event manager script (see game_events.lua can get a list of all the events that need to be started at the current time by calling events_list[current_time].