Internal development notes, very slightly cleaned up and commented a week later.
TL;DR: first ever combat using the new V3 engine, and a first pass at new state machines required for it, plus the associated data model
A major milestone has been reached in V3 development! Combat is now again part of the game. As a first effort AI-controlled combat has been prototyped, making use of the new tools available in the V3 engine. Here’s a video showing a fight between officers and pirates, using a simple test weapon:
Half of the development time was devoted to thinking about what was the best approach for the basic V3 AI combat. In the end it was clear the current V3 engine is easily up to the task of replicating anything V2 had, and going way beyond that. The game went from not having any concept of combat to the state the video shows in 3 days.
Here’s the notes that show how my design process goes:
- proto combat "what to attack?": model for civ assigning and threat matching component per civ for faster world queries threat aggro: for now just based on player rep relationships, all enemies live in harmony between each other "how to attack?": aggressor comp+sys: entities that seek matching vulnerable entities, tries to apply attacks to them requires state machine: aggressor-machine sight/detection area (model tied to attack) walking into range stance: hostile toggling (w/ neutral to come later) cooldowns (readyness check on attack model/comps) aggro vs defensive behavior vs BusyC aggression overrides BusyC: aggressor-machine will ignore previous BusyC and override it with the target entity this allows to automatically cancel jobs and other lower prio stuff, which check BusyC mutual ownership aggression does not check mutual BusyC ownership since that would preclude multiple entities attacking the same target if vulnerability is a separate system/SM... ... another "I am currently busy attacking" flag is required, and it must be natural for the ECS system and other systems/SMs but how to toggle behavior exclusivity for defense? there is an implicit hierarchy of urgency damage reception reaction > flee/heal/port/etc to stay alive > attack (if enabled) > feed/sleep/etc > work idea: a priority level for BusyC this would be an additional check before overriding BusyC: "impose our own BusyC only if our priority is higher" it keeps the systems 100% decoupled: vulnerable-machine does not need to know about aggressor-machine yet it interoperates naturally and it still works naturally with the current job system, which can opt-in to priorities later on idea: a system id for BusyC indentifying the current controller it allows to answer "am I the currently controlling system/SM for this entity?" with total independence over the actual BusyC and other state "shape" with prio and system id, the urgency hierarchy is solved, including future features like need fulfillement plus many lower level arbitration problems implement prio + owner hint for BusyC plus various ecs-* helpers "how to react to attacks?": vulnerable comp+sys: entities that can be damaged by attacks super simple proto for attacks, just model hp as a simple 0-100 autoregen value for now attack is simple random substraction defensive+damage reception behavior: separate state machine simple for now, it just activates a random "flee" walk when low on hp - "what is an attack?": formalize some model for how to damage a vulnerable entity sighting and range pose cooldowns damage points scroller no proc hooks, just pass and hardcode to attack-test
What I do is try to first write down what I want to accomplish, and then at another level of indentation, what is a possible idea or solution to the problem. Then I try to find counterproblems to the ideas, and keep editing the text as I go. If I end up implementing an idea, I update the text to match its final form and move on to the next item. If while implementing I realize the idea is bad, I delete the lines in the to-do file and try to come up with new things, or move to a different goal.
The big blocks of work were the state machines, enhancing the
BusyComponent system/protocol, and implementing a “fire and forget”-capable system for what is commonly called “procs” in RPGs.
Two new state machines have been introduced,
vulnerable-machine. Any entity can make use of them by adding their respective components.
In the future their behavior will be able to be customized by new weapons and enemies, as it now happens with the generic job machine. For now a very simple and much of a work-in-progress model for attacks has been implemented, along with the test beam-like attack shown in the video.
Another important change is introducing priorities and owners into
BusyComponent. BusyC is a generic way for systems to declare they are “in control” of an entity, for a loose concept of “control”. This has worked very well to coordinate work tasks. But combat (both wanting to attack, or being subject to damage) introduce higher levels of urgency. A work order shouldn’t take priority over fleeing from an attacker. While the job system already has an internal priority system, it’s not right to use it from a completely unrelated system. Indeed the goal is to make a priority system that has zero knowledge about game systems, but still allows them to cooperate.
This was solved very simply by making BusyC store a priority value, and an owner ID. Systems wanting to interfere with an entity must first the current priority of its BusyC component, and if it’s higher that their own, they must leave the entity alone. The owner ID allows for systems to know when a given entity is already busy with them, without having to declare new components just for this purpose (which is still a possibility of course).
- placeholder beam-like attack fx spawn entity with custom DS helper for reading weapon offset, test it - introduce scheduler-system runs at every frame (not phased!), careful with its usage scheduled-component ticks - at which game tick the proc must be called proc - a symbol to call with s e and optional data passed to the maker use refs for any entity passed, never pass ptr entity is destroyed after calling the proc - proper attack timing use two attack states in aggressor sm do-attack enables pose, creates scheduled proc with the right animation delay, then goes to wait-attact wait-attack for now just waits an arbitrary amount of time (equiv to a single sm iter) can't do better since aggressor sm is phase 5, decreasing phase increases CPU for little gain - port test attack beam to the new proc system
After some testing it was clear that the aggressive phasing the engine uses to lower CPU usage was too coarse to allow for smooth and precise playing of some animations and syncing them with effects. To fix this a new “scheduled proc” has been introduced, for one-shot execution of code at a precise moment. The aggressor machine was fixed to delegate the actual attack effects to scheduled procs, so they can be now frame-perfect.