NPC Engine Documentation ======================== written by Volker Lanz Release 3, 31. 5. 1999 ------------------------------------------------------------------------------- Index ------------------------------------------------------------------------------- 0. Disclaimer 1. What the NPC Engine does 2. Quick how-to to include the NPC Engine in your game 3. Advanced features 4. Functions 5. NPC_Engine properties 6. Entry points 7. Notes 8. How it works 9. Possible enhancements 10. Demo Story 11. Changes to the NPC Engine ------------------------------------------------------------------------------- 0. Disclaimer ------------------------------------------------------------------------------- NPC Engine is a library extension for the Inform interactive fiction authoring tool. It is provided "as is". The author takes no responsibility whatsoever for any malfunctioning of this code. All files included are Copyright (c) 1999 Volker Lanz. You may use these files in your own games freely. You may NOT alter any of the files and publish or distribute these modified versions. If you wish, contact the author via: volker.lanz@gmx.net Comments, bug reports, criticism, questions and suggestions are always welcome. ------------------------------------------------------------------------------- 1. What the NPC Engine does ------------------------------------------------------------------------------- The NPC Engine started out about a year ago when I was learning Inform and trying to make NPCs move in a complex way like in Infocom games such as Deadline and Suspect. I stumbled over the MoveClass library extension, which I found capable of doing many of the things I wanted to do. However, I still (as always) wanted more. So I began writing this one. Initially, there were lots and lots of routines scattered across various files, working, but nasty. I cleaned up a lot of it for the game Intruder which I published early in 1999. Again, I wasn't satisfied and reworked the whole thing. This library extension is now the result of that. NPC Engine does everything that MoveClass does (with the exception of random NPC movement, which I never had any use for). It also nearly does anything that follower.h can do, though not quite as sophisticated (following NPCs in cars, and example given in follower.h, wouldn't be possible with the NPC Engine right now, but could easily be implemented, if need be). That means, if you would like to have NPCs in your game that are moving through the playfield in a more sophisticated manner than just moving them from one room to another each turn on a predefined path and if you want the player to have the opportunity of following NPCs, this one is the right library extension for you. Additionally, it offers these features (I'm sure you can guess where the examples are taken from): * Complex descriptions of NPCs moving somewhere in the distance: Mrs. Robner is off to the north, heading towards you. Mr. Baxter, off to the south, opens a door and ducks into a room to the west. and even: You can hear footsteps on the staircase. * Asking the game itself and other NPCs where an NPC is: > MRS. ROBNER, WHERE IS GEORGE? "I last saw him just a minute ago. I don't know where he went, though." or > WHERE IS MS. DUNBAR? You haven't seen her yet. Note that you'll have to remove question marks from the player's input in BeforeParsing() to make this last one work. * Following NPCs you see moving in the distance. ... Mr. McNabb, off to the northeast, leaves your view to the east. > FOLLOW MCNABB Orchard Path ... Mr. McNabb is off to the east, heading to the southeast. * Looking through windows: Guest Room ... > LOOK THROUGH WINDOW ... To the east, Mr. Baxter comes into sight from the south. Mr. McNabb is to the east, examining his work. * Faster path finding The NPC Engine is quite fast in its finding of paths for NPCs. It uses a modified version of E. Dijkstra's shortest path algorithm which produces very little overhead. Additionally, the NPC Engine does not use objectloops to go through its classes but sets up a chained list of objects in each class to speed up loops. * Flexibility Various entry points allow the story author to modify the behaviour of the NPC Engine in many ways. ------------------------------------------------------------------------------- 2. Quick how-to to include the NPC Engine in your game ------------------------------------------------------------------------------- Assuming that you already have some skeleton of a game, follow these steps to intergrate the NPC Engine into your story: * Include the npc_engine.h file in your main game file. Do this right after including verlib.h. You probably know that the Inform Library is a bit picky about when you include what, so make sure to include npc_engine.h right there. * Call the function NPC_Initialise() from your game's Initialise() routine. This will set up all necessary variables and start the NPCs' daemons. Do not stop an NPC's daemon in your game unless you remove the NPC. You may pass one argument to NPC_Initialise() to set the type of following used. See "Advanced features" below. * Make all your NPCs inherit the NPC_Engine class. * Make all your rooms inherit the NPC_Room class. If you're sure that a certain room will never be walked into, through or out of by any NPC throughout the whole game, you may wish not to make it of the NPC_Room class. In large games with lots of rooms, this may speed up path finding. * If you do not already have a player object of your own, create one (copy the selfobj code from the library file parserm.h into your main game file and rename it to something useful, e.g. player_obj). Include the following lines to its each_turn property: ... each_turn [o; o = npc_first_npc; while (o) { if (TestScope(o, self)) o.npc_met = turns; o = o.npc_next; } ], ... * Set the global variable player to your player object in your Initialise() routine. * Be careful with your doors! Everything that applies to doors in code using MoveClass applies here. That is, you must not use the location global variable in your door code. Thus, instead of saying: ... door_to [; if (location == Hallway) return Living_Room return Hallway; ], ... you will have to say (equally working, but different to the NPCs): ... door_to [; if (self in Hallway) return Living_Room; return Hallway; ], ... * Also (and again like in MoveClass) you may never use (direction)_to properties of rooms to print strings. If you have something like this: ... e_to "There is only a short, blind corridor there.", ... you will have to do it this way to make the NPC Engine work: ... before [; Go: if (noun == e_obj) "There is only a short, blind corridor there."; ], ... without any e_to property. * Now you can move your NPCs by calling the NPC_Path() function. An NPC's npc_arrived property may deal with his arrival at a given destination. ------------------------------------------------------------------------------- 3. Advanced features ------------------------------------------------------------------------------- * Finding NPCs The NPC Engine includes a "find npc" verb. If the player ever met Mrs. Robner before, > FIND MRS. ROBNER will tell him when he last saw Mrs. Robner, or, if she is visible right now, tell him where she is. The player may also ask other NPCs where an NPC is: > GEORGE, WHERE IS MRS. ROBNER George will reply what he knows: If he met Mrs. Robner, say, an hour before, he'll say "I last saw her about an hour ago. I don't know where she went, though." If she's in the same room, he'll simply say "Ahem...". If he hasn't met her before, he will reply "I haven't seen her today." * Following NPCs The player may type > FOLLOW MRS. ROBNER if he can see her at the moment to follow her. The NPC Engine knows three different types of following: 1. Normal - The same technique used in the "follower.h" library extension: The same command is executed for the player that the NPC took the last move. Not useful if the player can see the NPC somewhere in the distance. 2. JUMP_FOLLOW_TYPE - If the player has seen the NPC moving the last move, he is directly taken to the location the NPC is now. Note that this is probably not so useful, since it will move the player through closed and even locked doors etc etc. This is, however, the way Infocom did implement the following in games like Deadline (check it out by following Mrs. Robner on her way from the living room to the master bedroom when she's going there to call back Steven - you can indeed walk through closed doors). 3. PATH_FOLLOW_TYPE - Probably the best solution to the problem. The player will move one location toward where he sees the NPC in question right now. A game may set which type to use by passing either JUMP_FOLLOW_TYPE or PATH_FOLLOW_TYPE to the NPC_Initialise() routine. Passing nothing will set the follow type to normal. * Looking through windows The simple action of looking through a window is actually quite hard to implement. The main problem about this is that if the player looks through a window, the response is printed and THEN the NPCs are moved, thus the player always gets information about what was true the last move. If accurate timing is important, this becomes unacceptable. The solution I chose to this may seem a bit awkward at first: The NPC_LookThroughWindow() doesn't print anything, but simply sets a property in all NPCs to the window that was looked through. When the NPC is moved aftwards, this property is checked and a message is printed, if the NPC is visible through the window. Follow these steps to allow looking through a window: - Add a looks_to property to the window, listing all the rooms this window lets the player see into: ... looks_to Corridor_1 Corridor_2 Corridor_3 Corridor_4, ... - Add a door_dir to the window, saying in which direction the windows looks: ... door_dir e_to, ... - Add a door_to property, saying which is the room directly on the other side of the window: ... door_to Corridor_4, ... - Finally, call NPC_LookThroughWindow() in the before() property for ##Search or ##LookThrough or what have you: ... before [; Search: print "You can see the corridor from here.^"; return (NPC_LookThroughWindow(self)); ], ... Note that since the NPC_LookThroughWindow() routine doesn't know yet if anything will be printed at all, you should always print some text not related to the NPCs moving, like in the example above. If you don't and if no NPC is visible, nothing will be printed else. This sounds probably quite complicated for such a simple thing, but if you go through these steps slowly and think about why they're all necessary, I'm sure you will get the idea behind it. Note: An NPC's daemon must be active to make this NPC visible through a window if he is not moving. This is one of the reasons why NPC's daemons should never be stopped as long as the NPCs are still on the visible playfield. * Stopping NPCs on their path to somewhere If you want some action of the player to stop the NPC temporarily on his path, you can achieve this goal by setting the NPC's npc_stopped property to a value higher than 1. E.g., if you wish to stop an NPC if the player says > HELLO MRS. ROBNER you would add the following to Mrs. Robner's before() property: ... before [; ... Hello: self.npc_stopped = 2; "Mrs. Robner is facing you."; ... ], ... This will stop Mrs. Robner from following her set path for two moves. The NPC Engine will print a default message ("Mrs. Robner starts to move about distractedly.") one move before she'll walk off again. If you want the player to be able to stop her from moving by talking to her, you may wish to add a self.npc_stopped = n; to her life's property ##Ask reaction, too. * Altering the default NPC Engine messages You can change any message the NPC Engine prints by replacing the routine NPC_Msg() from npc_engine.h with your own, modified version. Every message printed by the NPC Engine is in this routine (apart from error messages). ------------------------------------------------------------------------------- 4. Functions ------------------------------------------------------------------------------- * NPC_Initialise (type_of_following) Used to initialise the NPC Engine. You must call this from your game's Initialise() routine. You may pass one argument: The type of following you want to use. If you do not pass an argument, normal following is used. * NPC_Move (npc, destination, action, object) This moves an NPC to a destination, using a certain action and a certain object (exactly like MoveNPC in follower.h works). It also prints a description of the NPC moving. If you want to remove an NPC from your game, call NPC_Move(npc, 0);. * NPC_Path (npc, destination) Calculates and sets an NPC on its path to a destination; keep in mind that the NPC will only start moving if its daemon is active. Note that calling NPC_Path with the arguments used in moveclas.h is still valid. The arguments ifblocked and use_best, however, don't do anything. This one may be replaced by game code, because it only calls another function called NPC__Path() (note the two underscores). Replacing this is particularly useful if you have a set of pre-defined paths that you want to be checked first and call NPC_PrePath() if any of them can be used; only if none of them fits you then call NPC__Path(). * NPC_PrePath (npc, array, number_of_entries) This works exactly like NPCPrePath in MoveClass. You must pass an NPC, an array of stored destinations (e.g. "e_obj, e_obj, n_obj" and so on) and the number of entries in said array. * NPC_Schedule (npc, destination, delay) Schedules the movement of an NPC to a certain destination using the NPC's timer. Give the delay in moves until you want the NPC to begin moving. * NPC_LookThroughWindow (window) Used to look through windows. See "Advanced Features". * NPC_FindPath (npc, destination) If you should ever have to, you can make use of NPC Engine's path finding capabilities without actually starting a movement by calling this one. Pass npc and destination. The npc object will have the directions necessary in its npc_dirs property IN REVERSE ORDER. The npc_path_size property contains the number of entries. * NPC_Msg (action, message_number, object1, object2) NPC_Msg() prints all the NPC Engine messages. Replace it to modify the messages, if you want to. * NPC_Error (error_number, object1, object2) By replacing this routine with an empty stub-routine, you can keep the NPC Engine from printing any error messages. May be useful in release versions of games (MAY be...). ------------------------------------------------------------------------------- 5. NPC_Engine properties ------------------------------------------------------------------------------- The following is a list of all the important properties each object of the NPC_Engine class has: * npc_before_action() Called each time before the movement of an NPC. Should return true if it printed something to help notify waittime. * npc_after_action() Called each time after the movement of an NPC. Should return true if it printed something to help notify waittime. * npc_arrived(room) This one is being called when the NPC arrives in it target room. It gets passed the room the NPC is now in. Usually, this one is used to schedule the next path the NPC takes. * npc_walkon() Called when an NPC enters the room the player is in to notify of his movement. * npc_walkoff() Called when an NPC leaves the room the player is in to notify of his movement. * npc_door_open() This isn't a property of the NPC_Engine class. Instead, each door in your game that an NPC may walk through needs to have this. It just has to open the door, if need be, and return true if it did just that: ... npc_open [npc; if (self hasnt locked) { npc.npc_door_passed = self; give self open; rtrue; } rfalse; ], ... As you can see, the property also stores the NPC it was passed in the npc_door_passed property of that NPC. This is necessary to enable NPC Engine to notify the player of this door opening. ------------------------------------------------------------------------------- 6. Entry points ------------------------------------------------------------------------------- These are the entry points the NPC Engine calls. Replace any of these functions in your own code to modify the behaviour of the engine. Note that some of these already do some stuff, so you should always copy the originals from the npc_engine.h file and modify these instead of writing completely new functions. * NPC_AfterPrintVisible (npc, current_room) Is called if the NPC Engine tried to print a movement description for an NPC, but couldn't since it concluded that the NPC is not visible to the player. This one could be used for "You can hear footsteps on the staircase." stuff. Arguments passed are the NPC in question and the room he is in. Must return true if it printed something. * NPC_BeforeCheckVisible (current_room, test_room) Before the visibility of a certain room from a certain location is checked, this routine is called. If it returns true, the NPC Engine will assume the room is not visible. current_room is the room the visibility is checked from, test_room is the room for which it is checked. * NPC_BeforeFollowing (npc) Is called before an NPC is followed. If it returns true, following is not allowed. * NPC_AfterFollowing (npc, previous_location_of_player, success) After following an NPC, this routine is called. Arguments are the NPC in question, the previous location of the player before following and 1, if the following was successful or 0 if not. ------------------------------------------------------------------------------- 7. Notes ------------------------------------------------------------------------------- * The maximum number of NPCs the NPC Engine can deal with is 32. This could be extended if need be. I just couldn't think of a game that needed more. * The NPC Engine uses the NewRoom() Inform Library entry point. If you need this entry point yourself in your game, rename the NewRoom() function to something else meaningful (e.g. NPC_NewRoom()) and include a call to this renamed function in your own NewRoom(). * If you use timewait.h, delete or comment out the tw_waiting object definition in npc_engine.h. Because timewait.h has to be included after npc_engine.h, the header file cannot know if you're going to include timewait.h or not. So this is the only possible solution. * The NPC Engine doesn't care whether you use time or moves in your story. The standard replies to the "where is"/"find" question, however, use one turn for one minute. If time passes quicker or slower in your game, you will have to alter this. * If you really use calculated paths extensively, there may be still a certain delay in your game on slower computers. However, because most of your NPCs aren't moving most of the time anyway (or, to be precise, paths needn't to be calculated for these NPCs every move, but only at the beginning of each new movement), using ten to fifteen NPCs shouldn't even cause a noticable delay on fairly decent machines. I used MoSlo to test the Engine, set the speed of my p2-333 to 10% and played a demo game with seven NPCs that are moving quite a lot. It was slower, sure, but it wasn't too annoying (waiting four and a half hours in the game took about thirty seconds while it took over three minutes with MoveClass version 7). * At the moment, the player can never see through doors, may they be open or closed. This is an intended behaviour. (Mail me if you can't live with that.) * The NPC Engine uses the daemon property of each NPC object. This, of course, means that you cannot use this property in your game. You may want to use the npc_before_action() and npc_after_action() properties instead. * No descriptions are printed for NPCs that aren't moving. The NPC's own describe property may do that. On the other hand, while NPCs are moving, they're given the concealed attribute to prevent describe from being called by the library. This avoids things like: Living Room ... Mrs. Robner is sitting here knitting. ... Mrs. Robner heads off to the east. ... * At the moment, the NPC Engine does not support pluralname objects in its standard messages (does anyone use pluralname objects with NPCs?). ------------------------------------------------------------------------------- 8. How it works ------------------------------------------------------------------------------- * Finding paths Finding paths in NPC Engine is achieved using an algorithm by the Scandinavian scientist E. Dijkstra. The idea behind it is that you start off with a source room, mark it explored and give it a label of 0. Then you go through all the rooms reachable from the first room, mark them explored and give them a label of 1. The next loop, you go through all neighbours of rooms labelled 1, mark them explored and label them 2 and so on, until one of the rooms you find on your way is the destination room. If you store the direction you came from and room name with each explored room, you get the shortest path to follow simply by going from the destination room backwards. This is the reason why npc_dir entries are stored in reverse order by the NPC Engine. * Following NPCs with the PATH_FOLLOW_TYPE A fake NPC is created and the shortest path to the room the NPC who is being followed is calculated. Then, one step is taken on this path. * Looking through windows To allow for this, a fake room is moved next to the room on the other side of the window that is looked through and it is pretended as if there was a passage between these two rooms. The NPC movements are then described as if the player was in that room. * Optimising speed One problem with calculating paths in larger games is the objectloop(x ofclass Y) command in Inform. This command goes through all objects in the game and tests them against the class in question. If you have some five- hundred objects, this becomes really slow if it's done a few times between player inputs. The solution I chose was to cascade all rooms and all NPCs once at the beginning of the game and go through these only if necessary. If found that this heavily improved performance. ------------------------------------------------------------------------------- 9. Possible enhancements ------------------------------------------------------------------------------- * Already finished, tested and fully working is an enhancement that needs some hacking of the original Inform Library files and therefore wasn't included in this library extension: The ability for the parser to remember which NPC you currently talk to, offering the possibility to say: > HELLO MRS. ROBNER Mrs. Robner is facing you. > TELL ME ABOUT GEORGE (said to Mrs. Robner) "A child. He may be a man in age..." Should you really need this, please let me know. * Something I tried and didn't get to work the way I really wanted it to was the addition of "fake windows". Imagine a balcony looking onto a garden. You would of course want to have NPC movement descriptions for the NPCs in that garden. This could be achieved by a fake window on the balcony that has an each_turn property calling NPC_LookThroughWindow(). The problem is that you get descriptions of non-moving NPCs each turn with this technique, certainly quite annoying. I toyed around with this for a while, but didn't find any satisfying solution that was easy to implement without hacking too much of the original library code. * As well finished is an enhancement of waittime.h that allows to wait for an NPC. This may be released in the near future if I find the time to get in touch with the various authors of waittime / timewait. ------------------------------------------------------------------------------- 10. Demo Story ------------------------------------------------------------------------------- This release of the NPC Engine includes a small demo story in source and binary format to show the use of the engine. Note that, although this demo game features rooms from the game "Intruder" (which can be downloaded from ftp.gmd.de/if-archive/games/zcode/intruder.zip), it doesn't have any objects other than the rooms themselves and the NPCs aren't from "Intruder". ------------------------------------------------------------------------------- 11. Changes to the NPC Engine ------------------------------------------------------------------------------- 3.990531 * Now works with Inform 6.21 and Library 6/9; complies to strict error checking rules * Minor modifications and enhancements to the manual * Demo game added * NPC Engine now uses TestScope() instead of comparing ScopeCeiling()s where possible 1.990423 First public release ------------------------------------------------------------------------------- Did you find this manual easy to understand? Did it answer all the questions you had regarding the NPC Engine? If not, please mail me your questions regarding the NPC Engine and suggestions for improvements both to the engine itself and to this document.