I’m working on an RPG, and I’d like to give our writers the ability to arbitrarily execute game logic, so that they can move NPCs, change quest flags, or otherwise manipulate the game state from within dialogues. I’ve come up with and implemented an incredibly bare-bones scripting language which allows this, but I’m literally inventing things as I go, and since this involves an awful lot of token-parsing and string comparison, it’d make me feel a lot better if I could get some feedback on my design. It’s worked perfectly thus-far, but since it’s using C# to directly compare strings instead of making use of a dedicated lexer generator and parser generator, I’m concerned that there are tons of hidden gotchas just waiting to cause problems under the surface.
“Scripting commands” in my system start out as a generic string that gets sent to a scripting interpreter. The interpreter then has three jobs, in order: it parses the string into a space-delimited string[ ], checks if the string represents a well-formed command, and if it does, the interpreter uses a service locator to find the appropriate game APIs and executes its logic as-requested.
Determining if the string is well-formed is a two-step process. The first word in any command must identify what kind of logic it executes: SpawnCharacter, MoveCamera, AddItem, and so forth. I have a CommandType enum containing every acceptable type of command, so I test the first word by trying to convert it from a string to an enum. If it converts cleanly, and the interpreter has logic accompanying that command type, it runs a second check on the remainder of the string which is different for each command type. SpawnCharacter commands, for instance, must have exactly two words following “SpawnCharacter,” and of those two words the first must match a valid character ID, and the second must match a valid spawn point.
If all of the above conditions are met, the string gets handed to some ExecuteSpawnCharacter(string command) method which knows how to turn the string into an ID + spawn point and invoke the spawn manager to actually place the desired guy in the desired location. If any of the above conditions are not met at any time, the entire test fails upwards, and the scripting interpreter rejects the string.
That’s all there is to the entire system. I like that it’s fairly extensible (adding a new command is as simple as adding a new entry to my CommandType enum, and a function to handle that command type to the interpreter), I hate that actually sanitizing incoming strings involves tons and tons of converting strings to enums, and comparing string to string (e.g. if(characterID == “Bob”)). The degree to which this ties the scripting interpreter into the rest of the game’s architecture is also a little worrying, but since I access all game functionality through public interfaces retrieved via a service locator, and not through direct references, even that is technically decoupled.
So am I missing any big problems with this approach? It is so clumsy and frankenstein-y compared to a properly-designed scripting system that uses purpose-built lexers and parsers, but it doesn’t have any performance issues (I can interpret 1,000 commands/tick with no lag, in gameplay it’ll be interpreting 3-5 commands once or twice every few minutes). My instinct is to shrug and say “It’s technically not great, but if it works, it’s modular, and it’s extensible, that means it’s good enough,” but since I have very little formal software training I’m concerned that this is an architectural implosion waiting to happen.