Hacker News new | ask | show | jobs
by annywhey 2840 days ago
In short: sum types.

If behavior is controlled from the top level(as is suggested by the cases where the main loop grows to 10's of thousands of lines) your behavior branching also appears there. The data is still driving everything, but if the data model is inherently complex it bubbles up to the surface. And you can use sum types(whether provided by the language or in less structured "match a magic constant" type deals) to check your coverage. During prototyping, you can lean on dynamic types instead and get a similar outcome with more fluidity in model changes and lower debuggability.

That feels instinctually wrong if you're dosed up on polymorphism and expect a kind of "ultimate abstraction" where any of the enemy types could be totally different and you just write to an interface, but - the thing you want, and that is the practical situation in most gameplay logic, is not "wholly bespoke behavior per type of enemy, hide everything behind a facade and either duplicate code or use a complex indirection to reuse it where it's the same" but "98% of the behavior is on the same code path and reads top to bottom, except for the one or two important pieces of business logic that isn't". The latter is both more compact in total SLOC and by being surgically precise, gives more visibility into what the code does at the moment you try to debug or extend it - which is what we're really aiming for.

And if you have really complex behaviors, you do end up with Turing-complete script logic embedded in the data, just naturally growing out of data model extension to cover FSM logic. That indicates that you are never really "giving up power" at any point in this process - you're just gradually moving things up the abstraction chain in a controlled fashion, until your main loop is really an interpreter that deals with "opcodes" and "commands" more than it does any particular algorithm or data layout.

And then when you get there, you can go, "well, now that I have a virtual machine, let's optimize around that..." and then you start writing some tech to lower the overhead of the interpreter...and it just goes from there. You never have to jump off that train, but you can usually ship something well before you get it even to the Turing-complete level of programmability, too.