Search Unity

Approach to d20-like turn-based combat system on ECS. Is there one?

Discussion in 'Entity Component System' started by Goldseeker, Nov 15, 2018.

  1. Goldseeker

    Goldseeker

    Joined:
    Apr 2, 2013
    Posts:
    42
    The way I see it there is no good way to implement d20-like turn-based combat logic using ECS paradigm.
    Here are my problems:

    1) the systems are kinda meaning-less as a lot of calculations need to be done only when command is being given by a user, which is reflected perfectly by fuction call and not really good with concept of always running systems, also sometimes you need to compute result, but not do anything with it just return it and show.

    2) there is little be gained from performance standpoint, the individual calculations are super small and there is rarely a need to make them in parallel

    3) for now Unity ECS is based a lot on hardcoded stuff - all the component have to have separate structs representing them, I either need a type for every thingy in game (feat, weapon-type, stat) or need to have a component that kinda encompasses them all, but there can only be one component of the same type on the entity, so my character entity can't have +1 to attack with onehanded weapons and +1 to attack with twohanded weapons at the same time, so I need to associate feats with character they are on by Entity structs, which throws all the cache-friendliness out of the window.

    4) I really miss polymorhism in impelementing many of the rules that are present in the system, I either duplicate code a lot, or do a lot of switch-case statements in implementation

    So question is. Are there any good approaches? Any examples of them?
     
  2. Micz84

    Micz84

    Joined:
    Jul 21, 2012
    Posts:
    451
    ECS is just a tool and not magic one too. Of course there are some games that benefit greatly from ECS for example RTS and large scale summations. Turn based games benefit less because you have more time to do your stuff, but more complex turned based games can benefit too. One of additional benefits is less battery consumption on mobile.

    Currently there is no example for any kind of game. Only twin stick showers one, but it is very simple. ECS is very early in development.
     
    Antypodish likes this.
  3. Vuh-Hans

    Vuh-Hans

    Joined:
    Sep 24, 2013
    Posts:
    42
    I've found doing things that inherently require random access to memory (like tree searching with a priority queue) or things that require reactive iterations before proceeding (like Magic The Gathering's effect stack) to be hard to implement with ECS.

    However, something that helped me regarding reactive systems, like your first point, is to think of ECS as an assembly production line that is constantly running, but doesn't necessarily do stuff on each frame. In this metaphor, you can think of user input as a thing you put on the assembly line whenever the player finally does something, and it just gets processed on the next tick automatically.

    As an example, with my latest project (a grand strategy board game) I process the game's logic in a separate simulation world that is constantly ticking with each frame, yet most frames it doesn't alter its state at all. Since board games usually just react to user input, most of the time the systems are just "resting". This is the "model" of the game, and contains the entire state relevant for the game simulation (unit entities, map tile entities, player stats entities, etc). The "view" and "controllers" (which in my case are classic Unity game objects, mostly UI stuff) react to user input by creating a "command" entity, like SelectUnit, MoveUnit, AttackUnit, etc. The command entity gets processed by the simulation world the next frame and the systems make sure to mark the entities that changed. Then a couple of synchronization systems update the view game objects with the newly produced game state.

    You are right in that for some things there is no performance increase. Say only a single player gets to move units around the board at a time, then the simulation is processing a single MoveUnit command each time. It's the equivalent of having a for loop to do a single iteration, a waste. However, because of how ECS systems are iterating over entities, you can easily expand it to handle thousands of MoveUnit commands per frame in an efficient way.

    The key is to find a way to accommodate your game's logic with this assembly line style of coding.
     
    MegamaDev likes this.
  4. meanmonkey

    meanmonkey

    Joined:
    Nov 13, 2014
    Posts:
    148
    That's not entirely true, each system automatically stops running if there are no matching components found. You can add/remove components at any time in the mainthread. For example, if you want an entity to stop rendering, you just remove the meshinstancerenderer component. If there are no entites with renderer components, nothing is done at all. That counts for every individual system / component you write.

    Thats true, its' totally case sensitive. Don't get me wrong, I don't wanna sell you ecs at all cost, it has to fit your needs, that's most important :)

    enums are your friends :D

    Code (CSharp):
    1. Enum WeaponType
    2. {
    3. 1hand,
    4. 2hand
    5. }
    6.  
    7. struct PrimaryWeapon: IComponentData
    8. {
    9. WeaponType weaponType;
    10. }
    11.  
    12. struct SecondaryWeapon: IComponentData
    13. {
    14. WeaponType weaponType;
    15. }
    16.  
    17.  entityManager.AddComponentData(entity, new PrimaryWeapon{ weaponType = WeaponType.2hand });
    18.  
    19.  
    Can you give me an example ? I never had to code duplicate in ecs.

    I nice ecs presentation for small things:
     
    Last edited: Nov 16, 2018
    Attatekjir likes this.
  5. That's the point. Systems react to a set of components. If those components don't present system does not run. Easy. Putting a predefined set of components (or one final one in the set) is like firing an event. It commands the system to run on the entities involved. Systems aren't always running. They are running until they have entities with the component set defined.

    You can even do the small, you do them faster -> CPU will rest. When we talk about mobile, it will consume less battery. If it's desktop or console, it will run on lower temperature. And when you do monster-things with giant animations and moving parts, you have more wiggle-room to do so.

    These are one and the same in reality.
    If you need to copy your code in DOD, you're doing it wrong. You probably don't do it fine enough (small things to do). You're too coarse. Or you're trying to think in objects and not in processes.
    In your example: you don't care about what causes the +1 attack. You put the data when the player gains the +1 and remove it when the player loses it.

    Also you shouldn't aim for the perfect cache-friendliness, you should aim for the high enough cache-friendliness as possible. Which means don't break a sweat if one function which is called in every half an hour is not cache-friendly. When most of your application which is running in all or most of the frames are cache-friendly, it's okay.
     
    Last edited by a moderator: Nov 17, 2018
    Antypodish likes this.
  6. Goldseeker

    Goldseeker

    Joined:
    Apr 2, 2013
    Posts:
    42
    Okay people, thank you for all very good points, the thing is I kinda know them already, the hard part is when I want to apply the ECS principles it feels like I'm doing just to adhere to ECS paradigm, not because it is better structured way of coding or at least more performant. You don't need to sell the ECS paradigm for, I've more or less bought it for most cases, but turn-based complex logic is kinda hard to realize in ECS for me. I love that the post sparked at least some discussion, as I think it is really important to work on best practices in the ECS paradigm, as it is important to have best practices developed before ECS is ready for any kind of large scale uses by Unity community.

    I'll try to comment on particular things that you've said and maybe present a case that I'm trying to do and that I can't figure out how to do well in ECS paradigm:

    That is not entirely true, the ECS implementation still needs to check if system should be run (check if at least one of the groups requested is not empty) and that is even if you ignore the case when you can't filter out whether systems needs to act or not by components alone, in cases when you need match values in different groups for examples and if they match execute something.

    They're actually my enemy - they're hardcoded, no hope of extending the system without modifying the enum and the system. Also I hate switch cases.


    In cases of attack bonus, armor class bonus calculation in 99% cases the time you spent creating jobs and waiting for them is longer that the time that actual computation takes place.

    That is actually not correct, I do need to know what gave me that +1 so that I can present it to the user in the combat log, also complex mechanics like "bonuses of the type don't stack" require me to track where the bonuses come from, and actually in case of D20, it is much more prudent to just calculate it every time it is needed as the game rules are incredibly complex and literally anything can affect anything given right circumstance.

    Being too fine is detrimental to cache friendliness, both because it will require you to associate diffent entities instead of having components on the same entity and because having large component that contains all the data needed for calculation is just more cache friendly than doing calculation on smaller components

    So now to the case I'm trying to do:

    In d20 to determine result of the attack you need to 1) calculate Attack Bonus(AB) of the character attacking 2) calculate Armor Class(AC) of the character detecting. Both AB and AC are very fluid things that can be affected by lots of thing, character level, talents, buffs, equipment, terrain, enemy type, terrain type, battlefield situation etc. and any combination of all the factors. What I need to do for this experiment is have to characters standing next to each other, have AB and AC calculated and print it out in any form with maximum detail. In addition to that I'm thinking of how to implement equipment for example equiping weapon actually changes a lot in AB calculation, and magic items can have unpredictable and complex effects.

    So I'm thinking that ECS way would be to have:
    1) CharacterEntity - some entity that associates different aspects of the character together (it can't actually contain them as components as different aspects actually contain the same components, like all buffs have timers)
    2) AttackRequestedComponent - gets created as a separate entity when the attack is being requested by UI for example, the systems that calculate AB and AC react to the presence of this component and find characters involved and execute thier logic
    3) Various Character Aspect Components, that represent things like feats, equipments, buffs etcs, that are associated with Character by Entity struct

    On the system side the following happens:
    1) There are systems associated with their CharacterAspectComponent that react on AttackRequestedComponent(it contains links to both attacker and defender and maybe some info on attack itself, like ability used and stuff) calculate their respective AB/AC modifiers and create a new entity with AttackResultComponent that is linked to AttackRequestedComponent entity and contain respective modifier
    2) There is a system that combines all the AB and AC bonuses, applies stacking rules and output all the info in some way.

    It seems that it can work, but there things in it that rub me the wrong way:
    First of all, most of those systems need to filter their groups before doing things (for example Base Attack Bonus calculation system needs to have a group to find AttackRequestedComponent and a group of BABAspect components and need to filter out those BABAspect components that are associated with attacked entity found in AttackRequestedGroup)
    Second of all because of all the links all over the place there is no cache-friendliness here, just none
    Third, it seems that lifetime of things is a nightmire to manage in non-trivial cases, deleting character requires deleting need to delete tons of stuff which in turn may require deleting even more stuff.
    Forth, I am afraid that all this entity creation, entity filtering, and stuff will actually be detrimental to performance compared to OOP, as at least association in OOP case is done once and calculating AB/AC modifier requires one virtual method call

    Feel free to critique my approach and reasoning and I'm looking forward to hearing your takes on this problem
     
  7. Forgive me, I don't have time to react all of the text you presented, so I will just reply to the most important (I think) one.

    You're thinking about this wrong. You're thinking in objects. You don't write logs where you do calculations. LogWrite system is completely different from the system which calculates your attack summary. Or whatever.
    If you want to build a proper application in DOD you will really need to break down your application data-flow into small, bite-sized pieces and plan how to feed each piece to do its work.
    And do not try to do more work than necessary in one system. The single responsibility principle is much more important in DOD than it is in OOD. This way you will end up truly reusable small pieces of code and you can apply them in any situation when you need to do such calculation. But for example as soon as you tie it to the log write, you will be able to use this in one place. (crude example, but will do).

    I highly doubt it. Although it depends on what do you do exactly and how you design your data-flow. But simple linear calculation can be done in OOP as well if you're willing to give up the virtual calls and storing the data all over the place. If you store them in a tight array, you can do the same in OOP as well (you won't have cache misses).

    The next video is CPP DOD, but a lot of things can be applied to Unity and C# as well. Especially when he talks about the data-storage, the virtual calls and the data-flow design.
     
    Antypodish likes this.
  8. Goldseeker

    Goldseeker

    Joined:
    Apr 2, 2013
    Posts:
    42
    Of course I'm not gonna log in the system that calculates stuff, it is a bad idea both in OOP and in DOD, but there is a requirement to have a log so the data that can be logged should be preserved.

    Well applying BAB or other simple bonus, like from a simple feat is just an addition operation, posting a job can't be faster than addition.

    I hope you'll have time later to look at my proposed idea to approach the problem in ECS and the things I don't like about it.
     
    Last edited: Nov 18, 2018
  9. Depends. How many character will fight? If two, then yes, you probably should go with OOP. Or just move all your data in flat arrays and calculate that way.

    It depends.
    If you checked out the video I linked above. The guy talks about animation port from OOD to DOD. If you have a few types of things which can be described statically, and you can have more characters than these types, it's usually worth to move to DOD. If it's the other way around (creating DOD for two or three characters does not worth the effort).
    Like driving two or three animation is okay by OOP, driving hundred is not.
    Question is, how many times you switch context in your OOP code and how many times you would do the same in DOD?

    You're designing an OOP class. It's not DOD. Object with properties -> OOP.
    What's the difference between this CharacterEntity and the Character class if you were designing it in OOP?
    Almost nothing, maybe you just don't put the logic inside the class. So you're doing it wrong.

    If you think your game will be beneficial of DOD, then start from scratch. Forget the entity and systems and everything. Which part will be executed the most? And when I ask for 'part' it's an atomic calculation which cannot be break down anymore. Like adding up modifiers from an array to a value.
    Construct this core first. Then find out how to feed this value to the decision making (keeping everything modular, so you can put the decision making onto any pair of entities and it will have results if the input data is okay).
    Then start to think how to feed this system with data, what do you need? Start making entities which can hold this data (not a player object, an entity which has the data to feed into the main calculator system). Then when you have a series of systems in chain and if you feed it with const and spit out good result, you can start to work on how to interface it with a real game (how to translate your data into your systems).
     
  10. Goldseeker

    Goldseeker

    Joined:
    Apr 2, 2013
    Posts:
    42
    Well actually it would be rather different, it would be much more self-contained - the character class would actually contain its "aspects" and would have redirected methods to its "aspects" of appropriate type (if i were to make class design as flexible as possible)

    It sounds more like a mantra and less like an instruction or guidance. One thing is that I've already done decomposition following this principle as best as I'm currently able and either my decomposition is wrong and repeating it all over again will most likely not help me, or I did not present it very well, so I'll try to rephrase it from the systems point of view.

    1) simpliest systems - they calculate modifier for attack and defence based, there is one for every factor that can potentially affect those modifiers, they trigger on having an AttackRequestedComponent existing and calculate appropriate modifier for character involved in the attack, the result of the calculation is written by creating new entity containing the result

    2) A combine bonus system it aggregates all the bonuses related to the attack, for the simplicity sake just adding them up together and writes the result as an additional component to the same entity the AttackRequestedComponent.