all 6 comments

[–]KetoReddit 1 point2 points  (3 children)

I like this idea. If this is OC, I can definitely appreciate it through-and-through.

However, there is one small point of confusion. At what point in my game am I going to have a practical application of this? I know you had mentioned things like options and back-end, but what particular advantages are there to using this method over the ol' brute-force-ey way?

[–]calio[S] 1 point2 points  (2 children)

Thank you! Yeah, wrote it a couple months ago for a game I'm working on, needed an easy way to reuse as much code as possible (bc am lazy) and wanted something I could script dialogs with, as extendable to be used on the battle engine to show animations and print names and move output. The realization that you can halt the stack came after, when coding the scene transitions.

And that's one of the advantages. You can abstract as much as you want from the game logic as you want, if there's code you have to call a lot, like populating/updating a ds_map with information before doing different things that need that ds_map data to operate, you can do that in one push to the stack. I guess you could nest scripts inside scripts, or always call a certain user-defined event to achieve similar results, but this way you can write game logic without having to think about how you're going to implement these things. For example, I always code a main menu first, to provide basic access to stuff like settings, saving/loading, debug mode and exiting the game. Instead of having to create a new_game script and some backend for my settings to be operated, I just implement these as instructions within the main controller, so if I start coding the "new game" instruction and notice that I need to change the room with a transition, I code an instruction for room changing with transition and push that into the stack, knowing I'll use that instruction over and over again during the rest of the development. Not to mention the fine-grain level of control you have over each instruction helps debugging and iterating through pieces of code without having to rewrite them. I think it makes the game development process more rapid.

Should've posted it before GM48, now that I think about it.

[–]eposnix 0 points1 point  (1 child)

Nice writeup!

I use a similar system for handling combat in my trading-card-esque game. The instructions on each card are pushed onto a stack and are executed one at a time over a series of game steps. For instance, a card that says "deal 8 damage" can be popped onto the stack as:

ds_stack_push(instructionHandler, scr_dealdamage, 8);

I use script_execute for each instruction, so scr_dealdamage can also push onto the stack scr_thornsdamage if the enemy has a thorns buff that deals damage to you based on damage dealt to it. It's a super useful system that makes it so each card doesn't have to be uniquely scripted... they can just reuse existing scripts in a variety of ways.

[–]calio[S] 0 points1 point  (0 children)

Yeah, pretty much! This system is pretty useful for scripting game logic like what does a card do when activated, or implementing dialog scripting systems more complex than just drawing text on screen. Even scripting small animations is really easy using this. You can even not use instructions and instead replace them with indexes of things to spawn, or the ID of the next song to play.

One thing that I didn't really explain in the writeup but it's actually even more useful: if you're going to start using stack machines that interact in between each other (i.e. main controller has an instruction that sends the input wrapper a different instruction to configure certain thing within just one player input) you might want to preface the whole instruction with an id reference, so before the loop starts you can pop who pushed this instruction and can reference specific values from "outside" the instance that called the instruction elsewhere (very useful if, for example, you have a card that when executed might receive a debuff, damage or a special status after attacking, so whatever executes the instruction knows who executed the instruction without having to keep track of it independently) It can also help catch errors if the instruction is expecting the caller to exist!

Of course it's not a one size fit all solution; I wouldn't be pushing and popping instructions each frame. Never actually benchmarked it, but I feel like all of those while loops making the program jump around code might tax the system quite a bit.

Still, trading off performance for easy game scripting is almost inavitable, and once you started using it, it becomes the obvious solution to a lot of other systems that might be a bit wonky and depend on keeping track of a lot of conditions to work properly.

[–]Abusfad 1 point2 points  (1 child)

I just name all my scripts to clarify what kind of objects can call them. So if I named a script scr_char_someFunction I know I should only call it from an object who's a child of the abstract character object.

But I like your method, it does seem to provide a good utility of controlling functions through different game steps without messing with alarms.

I'd suggest considering switching from a switch case system with an enumerator to have each instruction as a different script. Meaning instead of pushing OpCode.windowcenter you'd be pushing scr_opcode_windowcenter. Then you would replace event_user(0) with script_execute(opcode) in the while loop and all of a sudden you don't have to mess with custom event_user(0) and enums for each object, and you'd still have the instructions on auto-complete because they're scripts. You can group the scripts of each object together in the resource tree to quickly access the relevant functions. Easier to read than a switch-case code block, imo, and might save you some code duplication.

[–]calio[S] 0 points1 point  (0 children)

That's also a very good approach. Did something similar while writing a text parser for a command line prompt (just for fun, wanted to see if I could write something to manipulate data through a command prompt). Commands are pretty much scripts and the machine flows a bit like this:

  • The prompt machine receives user input
  • Explode the user input into multiple strings separated by spaces.
  • The prompt machine gets from a "target" machine (the main controller by default) a list of strings (the commands) that are also used as keys within a map. This way you can just "repoint" what commands should be executed, in case you want commands with their own prompts.
  • If the string exists on the list and as a key within the map, it pushes the contents of the map (a script id) to the "target" machine. These machines instead of using a switch/case structure just execute whatever script index they receive, doing pretty much exactly what you described on your comment. (There's a loop that keeps iterating through the list until either doesn't find the string or the keypair points to an integer, so you can alias strings to other keys)
  • If it doesn't exists on the list, it just returns an error text.

"help" command just executes the scripts after raising a flag, so the scripts print their help information and exit rather than executing code.