Search Unity

  1. All Pro and Enterprise subscribers: find helpful & inspiring creative, tech, and business know-how in the new Unity Success Hub. Sign in to stay up to date.
    Dismiss Notice
  2. Dismiss Notice

[RELEASED] Turn Based Strategy Framework

Discussion in 'Assets and Asset Store' started by michal-zetkowski, Jul 2, 2019.

  1. Liberation85

    Liberation85

    Joined:
    Apr 16, 2019
    Posts:
    51

    I would be happy to do this, what address should I use?

    Hmm, the game does not seem to export very well for some reason, I can I just send the scripts to you or I can zip the whole asset folder up and send the whole lot?
     
    Last edited: Mar 8, 2021
  2. michal-zetkowski

    michal-zetkowski

    Joined:
    Mar 11, 2015
    Posts:
    163
    I'ts always better to see the level running instead of just looking at scripts :). If packed folder is not too big, then go ahead. Send it to support email: crookedhead@outlook.com
     
  3. michal-zetkowski

    michal-zetkowski

    Joined:
    Mar 11, 2015
    Posts:
    163
    @Liberation85 There was a bug in the code that I posted before, sorry. Already fixed the previous post, the code should look like this:

    Code (CSharp):
    1. var unitGO = Instantiate(unitToCreate, transform.position, transform.rotation);
    2. unitGO.transform.position = sourceCell.transform.position;
    3. unitGO.GetComponent<Unit>().Cell = someCell // you need reference to cell where the unit will be spawned
    4. FindObjectOfType<CellGrid().AddUnit(unitGO.GetComponent<Transform>());
     
    Liberation85 likes this.
  4. CashWasabi

    CashWasabi

    Joined:
    Dec 3, 2013
    Posts:
    1
    Hey,

    I just found TBD in my unity assets (I bought it in 2017) and it was a perfect match for my game idea I had this month. I've been reading the introductory PDF and I already am immensly thankful that you created this because it implements everything I needed but didn't know where to start with.

    Best regards!
     
    michal-zetkowski and ledshok like this.
  5. MrBIoBR

    MrBIoBR

    Joined:
    Jul 2, 2017
    Posts:
    9
    Hi there, i'm looking to buy this asset for a project of a "Chess like" game.
    I've seen a lot of discussions on implementing a diagonal movement ( 8 directions), how is that working as of right now?

    was diagonal movement implemented on the "main asset" or is there some "guide" on how I can implement it?.

    Best Regards
     
  6. sudahi51

    sudahi51

    Joined:
    Aug 17, 2020
    Posts:
    4
    Hey, what sizes are the tiles and unit sprites used in the example maps?
     
  7. Panhypersebastos

    Panhypersebastos

    Joined:
    Feb 21, 2017
    Posts:
    31
    I added a basic logistics system to my game "Egypt vs. Aliens". The system is based on the availability of water and has water radiate out from certain terrain types (river/ocean/oasis) in a diminishing manner:



     
    michal-zetkowski likes this.
  8. michal-zetkowski

    michal-zetkowski

    Joined:
    Mar 11, 2015
    Posts:
    163
    Hey, the short answer is that it is not implemented out-of-the-box, the solution is mentioned in the code and some details can be found here on the forum. I didn't implement in myself and I'm not sure how it worked out in the end. I'm positive that it is doable though.
     
    MrBIoBR likes this.
  9. michal-zetkowski

    michal-zetkowski

    Joined:
    Mar 11, 2015
    Posts:
    163
    Hey, all the demo scenes linked in Asset Store have assets used in them listed in their description on itch.io. The sprites are used as-is, so you can download them and check out the size.
     
    sudahi51 likes this.
  10. Untrustedlife

    Untrustedlife

    Joined:
    Apr 21, 2020
    Posts:
    39
    Development is going really well for me, i added floating indicators of what just happened which float up and dissappear and give exact damage numbers and such when a unit attacks and so on:
    upload_2021-4-7_14-6-2.png
    upload_2021-4-7_14-4-32.png
     
    michal-zetkowski likes this.
  11. Rehtael

    Rehtael

    Joined:
    Jun 20, 2017
    Posts:
    53
    Considering a purchase, but I want to know if this kit is able to use cover and line of sight features, like in the XCOM or Shadowrun games. If I have to make it myself, that shouldn't be too big of a deal, but if it's built-in, that would be amazing.
     
  12. Neil2TheKing

    Neil2TheKing

    Joined:
    Sep 14, 2017
    Posts:
    5
    Hi Michal, thanks for this framework, it's awesome! I used this framework to help create a two player game with online multiplayer (Photon Engine). Used RPC's mostly. If anyone needs help setting up online networking let me know and I can share the code snippets!

    Here's a link to the game if you're curious : )
    https://storytime.itch.io/anthology
     
    michal-zetkowski likes this.
  13. Untrustedlife

    Untrustedlife

    Joined:
    Apr 21, 2020
    Posts:
    39
    You will have to build it in yourself, look at the previous comments on this post, i posted while i was figuring it out.
     
    michal-zetkowski likes this.
  14. michal-zetkowski

    michal-zetkowski

    Joined:
    Mar 11, 2015
    Posts:
    163
    @Rehtael, it is as @Untrustedlife said. Let me know if you have any issues.

    @Neil2TheKing wow, looks impressive! I never did got around to implementing online multiplayer myself, I wasn't even sure what tools to use. Thanks for sharing this. If you find time to post your insights or the code snippets that you mentioned, I would greatly appreciate that :)
     
  15. Liberation85

    Liberation85

    Joined:
    Apr 16, 2019
    Posts:
    51
    Quick question, upon starting a new mission I would like one of my units auto selected.

    So for example, upon starting the mission I would want my main base unit to be selected the second the map loads.

    What would be the best way of doing this?
     
  16. michal-zetkowski

    michal-zetkowski

    Joined:
    Mar 11, 2015
    Posts:
    163
    Hey, to have the unit auto-selected just assign CellGridStateUnitSelected with the unit that you want selected as a parameter to cellGrid.cellGridState (obviously, you need to have a reference to cellGrid available)

    Code (CSharp):
    1. cellGrid.CellGridState = new CellGridStateUnitSelected(cellGrid, unitToSelect)
     
  17. GabrielHorn

    GabrielHorn

    Joined:
    Feb 9, 2013
    Posts:
    1
    Hello @michal-zetkowski!

    Thank you for creating this awesome asset. It's a great foundation for many kinds of projects.

    I wanted to ask for some pointers on how to start implementation of "special actions". This subject was raised a few times in this thread already, but as I'm still somewhat new to coding I need a slightly more in-depth hint(s) than just "code it in GridState" ;)

    For starters I wanted to add a "Guard" action (self buff that ends the unit's turn and lasts until the start of the unit's next turn) for all my units. It should work more or less like this:
    • "Guard" button becomes active/visible when a unit is selected;
    • When "Guard" button is clicked:
      1. a buff is applied to the selected unit;
      2. all ActionPoints of the unit are spent;
      3. the unit's turn ends;
    I think I have the first point figured out. When it comes to the second one I should be able to write a function that does all this, but I'm not sure where to put it within the framework. Should I add a new method (OnGuardButtonClicked()?) to CellGridState (or CellGridStateUnitSelected?) and then call it when the button's clicked?

    (Sorry if I sound like a layman, but it's the first time when I'm wrestling with someone else's code after finishing some basic and intermediate Unity/C# courses ;))
     
  18. michal-zetkowski

    michal-zetkowski

    Joined:
    Mar 11, 2015
    Posts:
    163
    Hey, happy to hear that you like it :)

    I would do it like this:
    • Add a "guard" button to your unit prefab, make the button deactivated by default
    • In CellGridStateUnitSelected.OnStateEnter activate the button (deactivate it in OnStateExit)
    • Wire up OnClicked on the button to some method on the unit
    • The method should activate the buff, set ActionPoints to zero, and probably reselect the unit with the following code:
      Code (CSharp):
      1. CellGrid cellGrid = FindObjectOfType<CellGrid>();
      2. cellGrid.CellGridState = new CellGridStateUnitSelected(cellGrid, this)
    You can check out how to apply buffs to units in TriggerSpecialAbility method on Hero from Example1.

    Let me know if that helped
     
  19. MCKoleman

    MCKoleman

    Joined:
    Oct 17, 2019
    Posts:
    2
    Hey, I bought this to get a head start on development for my TBS, but I've run into some issues with the cell detection and couldn't find documentation for or the actual code that handled cell selection. I found the declarations for the events that handle cell selection, but couldn't find any calls or functions that trigger the events. I am using 3D models and the cell detection is very unstable. Sometimes cells are detected without issue, other times I have to zoom in very close to the cell to be able to select it, other times I can only select it by clicking on the side of the cell. Also I can't seem to select units sometimes, and since I don't know where the code that handles this stuff is I have no clue how to make changes.
     
  20. michal-zetkowski

    michal-zetkowski

    Joined:
    Mar 11, 2015
    Posts:
    163
    Hey, sorry to hear that. Check out OnMouseEnter, OnMouseExit, OnMouseDown methods on Unit and Cell classes. They are called when mouse enters the collider, leaves the collider and is clicked on the collider. You could put some debug code there. These methods invoke events that are handled in CellGrid - next you could check out OnCellHighlighted, OnCellDehighlighted, OnCellClicked and OnUnitClicked.

    Let me know if you have more issues.
     
  21. MCKoleman

    MCKoleman

    Joined:
    Oct 17, 2019
    Posts:
    2
    Thank you, I was able to fix it! After placing debug logs all over the code you mentioned, it appeared that units were not being selected when clicking on the cell that they were on. I don't know if this was by design, but I was able to reroute some event handlers to allow for more intuitive input. So far the framework works great!
     
  22. michal-zetkowski

    michal-zetkowski

    Joined:
    Mar 11, 2015
    Posts:
    163
    Yes, this is how it works by design, glad you were able to fix it :)
     
  23. michal-zetkowski

    michal-zetkowski

    Joined:
    Mar 11, 2015
    Posts:
    163
    Hey everyone, lately I've been working on an update and would like to share the progress with you. The update fills in substantial deficiencies in the project and I feel that it will be a milestone in framework development. I expect to release it to Asset Store by autumn. I'm quite excited about the changes and interested to hear your opinion. Feel free to contact me either here or by email (crookedhead@outlook.com). I'll split each feature into a separate post, so it is easier for you to digest and reference.
     
    JAMiller, aweha and Jos-Yule like this.
  24. michal-zetkowski

    michal-zetkowski

    Joined:
    Mar 11, 2015
    Posts:
    163
    Turn resolver - A new component of CellGrid that dictates which player goes next and which units he can use in given turn. Before, this was hardcoded in EndTurn method of CellGrid. This change allows to easily implement speed-based transitioning - a commonly requested feature.

    Code (CSharp):
    1. public abstract class TurnResolver : MonoBehaviour
    2. {
    3.     public abstract TransitionResult ResolveStart(CellGrid cellGrid);
    4.     public abstract TransitionResult ResolveTurn(CellGrid cellGrid);
    5. }
    6. public class TransitionResult
    7. {
    8.     public Player NextPlayer { get; private set; }
    9.     public List<Unit> PlayableUnits { get; private set; }
    10.     public TransitionResult(Player nextPlayer, List<Unit> allowedUnits)
    11.     {
    12.         NextPlayer = nextPlayer;
    13.         PlayableUnits = allowedUnits;
    14.     }
    15. }
    16.  
    HOW TO USE IT?

    Implement TurnResolver or use one of the predefined resolvers and attach it to CellGrid gameobject. By default, GridHelper will automatically add SubsequentTurnResolver which works the same as current implementation. CellGrid will grab reference to the component and run ResolveTurn method in EndTurn function and ResolveStart method at the start of the game.
     
  25. michal-zetkowski

    michal-zetkowski

    Joined:
    Mar 11, 2015
    Posts:
    163
    Game end resolver - Another new component of CellGrid that checks if the game is over and which players are victorious and defeated. Before, this was hardcoded in a couple of different parts of CellGrid. Checkers that I already implement include:
    • TurnLimitCondition - game ends after a given number of turns
    • PositionCondition - game ends when a given position is reached
    • SurvivalCondition - game ends when a given unit dies
    • DominationCondition - game ends when there is only one surviving player, the same as in current implementation
    Code (CSharp):
    1. public abstract class GameEndCondition : MonoBehaviour
    2. {
    3.     public abstract GameResult CheckCondition(CellGrid cellGrid);
    4. }
    5. public class GameResult
    6. {
    7.     public GameResult(bool isFinished, List<int> winningPlayers, List<int> loosingPlayers)
    8.     {
    9.         IsFinished = isFinished;
    10.         WinningPlayers = winningPlayers;
    11.         LoosingPlayers = loosingPlayers;
    12.     }
    13.     public bool IsFinished { get; private set; }
    14.     public List<int> WinningPlayers { get; private set; }
    15.     public List<int> LoosingPlayers { get; private set; }
    16. }
    HOW TO USE IT?

    Implement GameEndCondition or use one of the predefined checkers and attach it to CellGrid gameobject. By default, GridHelper will automatically add DominationCondition which works the same as current implementation. CellGrid will grab reference to the component and run CheckCondition in the following situations:
    • When any unit dies
    • When any unit moves
    • On turn end
    If you needed to check for game over in any different situation you would need to call the checker manualy. Because of this, the solution is still a bit rigid. Let me know if you have any suggestions how to improve it.
     
  26. michal-zetkowski

    michal-zetkowski

    Joined:
    Mar 11, 2015
    Posts:
    163
    AI System - The Framework is finally getting an AI system other than NaiveAIPlayer script. It is possible now to create competent computer controlled players that are fun to play with. The system is based on AIActions that are attached to individual units. AIPlayer goes through each of its units and runs CheckCondition on each of the actions. If the condition is met, the action is executed. The basic actions that I implemented are:
    • MoveToPositionAIAction
    • AttackAIAction
    Code (CSharp):
    1. public abstract class AIAction : MonoBehaviour
    2. {
    3.     public abstract bool CheckCondition(Player player, Unit unit, CellGrid cellGrid);
    4.     public abstract void Precalculate(Player player, Unit unit, CellGrid cellGrid);
    5.     public abstract IEnumerator Execute(Player player, Unit unit, CellGrid cellGrid);
    6.     public abstract void CleanUp(Player player, Unit unit, CellGrid cellGrid);
    7.     public abstract void ShowDebugInfo(Player player, Unit unit, CellGrid cellGrid);
    8. }
    And this is how the AIPlayer executes actions

    Code (CSharp):
    1. private IEnumerator PlayCoroutine(CellGrid cellGrid) {
    2.   var MyUnits = cellGrid.GetMyUnits(this);
    3.   var UnitsOrdered = GetComponent < UnitSelection > ().SelectNext(MyUnits, cellGrid);
    4.   foreach(var unit in UnitsOrdered) {
    5.     unit.MarkAsSelected();
    6.     while (DebugMode && !Input.GetKeyDown(KeyCode.N)) {
    7.       yield
    8.       return 0;
    9.     }
    10.     var AIActions = unit.GetComponentsInChildren < AIAction > ();
    11.     foreach(var aiAction in AIActions) {
    12.       var shouldExecuteAction = aiAction.CheckCondition(this, unit, cellGrid);
    13.       if (DebugMode) {
    14.         aiAction.Precalculate(this, unit, cellGrid);
    15.         aiAction.ShowDebugInfo(this, unit, cellGrid);
    16.         while (!Input.GetKeyDown(KeyCode.A)) {
    17.           yield
    18.           return 0;
    19.         }
    20.       }
    21.       if (shouldExecuteAction) {
    22.         if (!DebugMode) {
    23.           aiAction.Precalculate(this, unit, cellGrid);
    24.         }
    25.         yield
    26.         return (aiAction.Execute(this, unit, cellGrid));
    27.       }
    28.       aiAction.CleanUp(this, unit, cellGrid);
    29.     }
    30.     unit.MarkAsFriendly();
    31.   }
    32.   cellGrid.EndTurn();
    33.   yield
    34.   return 0;
    35. }
    It is also possible to define the order in which the AI player will use its units. The default implementation selects units based on their freedom of movement, just like in the current version.

    Code (CSharp):
    1. public abstract class UnitSelection : MonoBehaviour
    2. {
    3.     public abstract IEnumerable<Unit> SelectNext(List<Unit> units, CellGrid cellGrid);
    4. }
    HOW TO USE IT?

    First of all you need to have a player with AIPlayer script attached. The script will pull AIActions from unit's children so that's where they need to be added.
    To implement a new action it is essential to code two methods: CheckCondition and Execute. Let's say you have a unit that has a healing ability - in CheckCondition you could check if units HP is below a given threshold and cast the healing spell in Execute.

    The purpose of other methods is mostly to facilitate debugging:
    • Precalculate - The idea was to have all the data needed by Execute precalculated, so it can be used by ShowDebugInfo beforehand. Another advantage is that Execute is not cluttered with calculations, just runs the action.
    • ShowDebugInfo - Displays debug information, either by visualizing it on the grid (as we will see in example below) or just printing to the console
    • Cleanup - Clears any state that was stored and returns the grid to normal

    EXAMPLE

    Let's take a look at MoveToPositionAIAction implementation.

    Code (CSharp):
    1. public override bool CheckCondition(Player player, Unit unit, CellGrid cellGrid) {
    2.   cellDebugInfo = new Dictionary < Cell, string > ();
    3.   cellGrid.Cells.ForEach(c => cellDebugInfo[c] = "");
    4.   var evaluators = GetComponents < CellEvaluator > ();
    5.   cellScores = cellGrid.Cells.Select(c => (cell: c, value: evaluators.Select(e => {
    6.     var score = e.Evaluate(c, unit, player, cellGrid);
    7.     var weightedScore = score * e.Weight;
    8.     cellDebugInfo[c] += string.Format("{0:+0.00;-0.00} * {1:+0.00;-0.00} = {2:+0.00;-0.00} : {3}\n", e.Weight, score, weightedScore, e.GetType().ToString());
    9.     return weightedScore;
    10.   }).Aggregate((result, next) => result + next))).ToList();
    11.   cellScores.ToList().ForEach(s => cellDebugInfo[s.cell] += string.Format("Total: {0:0.00}", s.value));
    12.   var (topCell, maxValue) = cellScores.Where(o => unit.IsCellMovableTo(o.cell))
    13.     .OrderByDescending(o => o.value)
    14.     .First();
    15.   var currentCellVal = evaluators.Select(e => e.Weight * e.Evaluate(unit.Cell, unit, player, cellGrid))
    16.     .Aggregate((result, next) => result += next);
    17.   if (maxValue > currentCellVal) {
    18.     TopDestination = topCell;
    19.     return true;
    20.   }
    21.   return false;
    22. }
    What happens here is that each cell is evaluated and given a score. If there is a cell with better score than the current cell, the unit will move there. I also collect debug data here, because it doesn't make sense to recalculate it later just for debug purposes.

    Evaluation is the interesting part. The base evaluator class looks like this:

    Code (CSharp):
    1. public abstract class CellEvaluator: MonoBehaviour {
    2.   public float Weight = 1;
    3.   public abstract float Evaluate(Cell cellToEvaluate, Unit evaluatingUnit, Player currentPlayer, CellGrid cellGrid);
    4. }
    The evaluators that I implemented include:
    • DamageCellEvaluator - evaluates position based on damage that can be dealt from there
    • DamageTakenCellEvaluator - evaluates position based on damage that can be taken there
    • DistanceCellEvaluator - evaluates position based on distance
    • UnitProximityCellEvaluator - evaluates position based on proximity of some other unit
    • AlliesNearbyCellEvaluator - evaluates position based on the number of alies that are adjacent to the cell
    Combining different evaluators and playing around with their parameters could encourage some pretty complex behaviour.

    Code (CSharp):
    1. public override void ShowDebugInfo(Player player, Unit unit, CellGrid cellGrid) {
    2.   (cellGrid.CellGridState as CellGridStateAITurn).CellDebugInfo = cellDebugInfo;
    3.   var minScore = cellScores.Min(e => e.value);
    4.   var maxScore = cellScores.Max(e => e.value);
    5.   foreach(var (cell, value) in cellScores) {
    6.     // Avoid 0 division
    7.     minScore -= float.Epsilon;
    8.     maxScore += float.Epsilon;
    9.     var color = Color.Lerp(new Color(1, 0, 0, 0.5 f), new Color(0, 1, 0, 0.5 f), value > 0 ? value / maxScore : value / minScore * (-1));
    10.     cell.SetColor(color);
    11.   }
    12.   if (TopDestination != null) {
    13.     TopDestination.SetColor(Color.blue);
    14.   }
    15. }
    In ShowDebugInfo each cell is coloured based on its score, top destination is marked in blue and cell scores are passed to CellGridStateAITurn - you can check them out by clicking on any cell.

    Code (CSharp):
    1. public override IEnumerator Execute(Player player, Unit unit, CellGrid cellGrid) {
    2.   TopDestination.MarkAsHighlighted();
    3.   var path = unit.FindPath(cellGrid.Cells, TopDestination);
    4.   List < Cell > selectedPath = new List < Cell > ();
    5.   float cost = 0;
    6.   for (int i = path.Count - 1; i >= 0; i--) {
    7.     var cell = path[i];
    8.     cost += cell.MovementCost;
    9.     if (cost <= unit.MovementPoints) {
    10.       selectedPath.Add(cell);
    11.     } else {
    12.       for (int j = selectedPath.Count - 1; j >= 0; j--) {
    13.         if (!unit.IsCellMovableTo(selectedPath[j])) {
    14.           selectedPath.RemoveAt(j);
    15.         } else {
    16.           break;
    17.         }
    18.       }
    19.       break;
    20.     }
    21.   }
    22.   selectedPath.Reverse();
    23.   if (selectedPath.Count != 0) {
    24.     unit.GetComponent < MoveAbility > ().Destination = selectedPath[0];
    25.     unit.GetComponent < MoveAbility > ().Act(cellGrid);
    26.     while (unit.IsMoving) {
    27.       yield
    28.       return 0;
    29.     }
    30.   }
    31. }
    Execute method is still a work in progress, most of the code should be moved to Precalculate. Towards the end of the snippet, the action is executed (there is even some foreshadowing of next new feature)
    Code (CSharp):
    1. public override void CleanUp(Player player, Unit unit, CellGrid cellGrid) {
    2.   foreach(var cell in cellGrid.Cells) {
    3.     cell.UnMark();
    4.   }
    5.   TopDestination = null;
    6. }
    Finally, in CleanUp method colours on the grid are cleared and top destination is nulled out.

    CONCLUSION

    To sum up, in my opinion new AI system turned out really nice and is a huge leap forward compared to NaiveAIPlayer that we had to put up with this far. The next step is to replace handcrafted evaluators with Unity MLAgents - that would be a real gamechanger for the framework.
     

    Attached Files:

    aweha likes this.
  27. michal-zetkowski

    michal-zetkowski

    Joined:
    Mar 11, 2015
    Posts:
    163
    Ability System - This is the answer to common requests regarding special ability implementation, another new system that I'm quite happy with. Usually the advice that I gave about it was to modify CellGridStateUnitSelected script. Obviously, this solution was not very elegant, not to mention that this script was pretty ugly in itself - it contained code handling both attacking and moving units. What's more, adding abilities this way affected only human players, the AI needed to get separate implementation.

    The soultion that I came up with is encapsulating abilities in a separate class that both human (through a new CellGridState class) and AI players (through AIActions) can use.

    Code (CSharp):
    1. public abstract class Ability: MonoBehaviour {
    2.   public Unit UnitReference {
    3.     get;
    4.     private set;
    5.   }
    6.   protected virtual void Awake() {
    7.     UnitReference = GetComponent < Unit > ();
    8.   }
    9.  
    10.   public abstract void Act(CellGrid cellGrid);
    11.   public virtual void OnUnitClicked(Unit unit, CellGrid cellGrid) {}
    12.   public virtual void OnCellClicked(Cell cell, CellGrid cellGrid) {}
    13.   public virtual void OnCellSelected(Cell cell, CellGrid cellGrid) {}
    14.   public virtual void OnCellDeselected(Cell cell, CellGrid cellGrid) {}
    15.   public virtual void Display(CellGrid cellGrid) {}
    16.   public virtual void CleanUp(CellGrid cellGrid) {}
    17.  
    18.   public virtual void OnTurnStart(CellGrid cellGrid) {}
    19.   public virtual void OnTurnEnd(CellGrid cellGrid) {}
    20.  
    21.   public virtual bool CanPerform(CellGrid cellGrid) {
    22.     return false;
    23.   }
    24. }
    Simply speaking, the class consists of three parts:
    • Methods used for human interaction that kind of mimic CellGridState interface: OnUnitClicked, OnCellClicked, OnCellSelected, OnCellDeselected, Display, CleanUp
    • Methods that mimic a small part of Unit interface: OnTurnStart, OnTurnEnd (probably more will be added (?))
    • Act method that executes the ability

    Basically, the idea is that abilities define what happens when they are used and how human players interact with the game to use them. The AIAction can grab the ability directly and just call Act method, while for human interaction I coded a new CellGridState class (actually in the end I'll probably just replace CellGridStateUnitSelected implementation for consistency)

    Code (CSharp):
    1. public class CellGridStateAbilitySelected: CellGridState {
    2.   List < Ability > _abilities;
    3.   Unit _unit;
    4.  
    5.   public CellGridStateAbilitySelected(CellGrid cellGrid, Unit unit, List < Ability > abilities): base(cellGrid) {
    6.     _abilities = abilities;
    7.     _unit = unit;
    8.   }
    9.  
    10.   public override void OnUnitClicked(Unit unit) {
    11.     _abilities.ForEach(a => a.OnUnitClicked(unit, _cellGrid));
    12.   }
    13.   public override void OnCellClicked(Cell cell) {
    14.     _abilities.ForEach(a => a.OnCellClicked(cell, _cellGrid));
    15.   }
    16.   public override void OnCellSelected(Cell cell) {
    17.     base.OnCellSelected(cell);
    18.     _abilities.ForEach(a => a.OnCellSelected(cell, _cellGrid));
    19.   }
    20.   public override void OnCellDeselected(Cell cell) {
    21.     base.OnCellDeselected(cell);
    22.     _abilities.ForEach(a => a.OnCellDeselected(cell, _cellGrid));
    23.   }
    24.   public override void OnStateEnter() {
    25.     var canPerformAction = _abilities.Select(a => a.CanPerform(_cellGrid)).Aggregate((result, next) => result || next);
    26.     if (!canPerformAction) {
    27.       _unit.SetState(new UnitStateMarkedAsFinished(_unit));
    28.     }
    29.     _unit.OnUnitSelected();
    30.     _abilities.ForEach(a => a.Display(_cellGrid));
    31.   }
    32.   public override void OnStateExit() {
    33.     _unit.OnUnitDeselected();
    34.     _abilities.ForEach(a => a.CleanUp(_cellGrid));
    35.   }
    36. }
    This setup allows to have multiple abilities active at the same time - like moving and attacking - or have abilities selected one by one - for example picked from a list.

    EXAMPLE

    Let's take a look at attack ability implementation

    Code (CSharp):
    1. public class AttackAbility: Ability {
    2.   public Unit UnitToAttack {
    3.     get;
    4.     set;
    5.   }
    6.   List < Unit > inAttackRange;
    7.  
    8.   public override void Act(CellGrid cellGrid) {
    9.     if (UnitReference.IsUnitAttackable(UnitToAttack, UnitReference.Cell)) {
    10.       UnitReference.AttackHandler(UnitToAttack);
    11.     }
    12.   }
    13.   public override void Display(CellGrid cellGrid) {
    14.     var enemyUnits = cellGrid.GetEnemyUnits(cellGrid.CurrentPlayer);
    15.     inAttackRange = enemyUnits.Where(u => UnitReference.IsUnitAttackable(u, UnitReference.Cell)).ToList();
    16.     inAttackRange.ForEach(u => u.MarkAsReachableEnemy());
    17.   }
    18.  
    19.   public override void OnUnitClicked(Unit unit, CellGrid cellGrid) {
    20.     if (UnitReference.IsUnitAttackable(unit, UnitReference.Cell)) {
    21.       UnitToAttack = unit;
    22.       Act(cellGrid);
    23.       cellGrid.CellGridState = new CellGridStateAbilitySelected(cellGrid, UnitReference, UnitReference.GetComponents < Ability > ().ToList());
    24.     } else if (cellGrid.GetMyUnits(cellGrid.CurrentPlayer).Contains(unit)) {
    25.       cellGrid.CellGridState = new CellGridStateAbilitySelected(cellGrid, unit, unit.GetComponents < Ability > ().ToList());
    26.     }
    27.   }
    28.   public override void OnCellClicked(Cell cell, CellGrid cellGrid) {
    29.     cellGrid.CellGridState = new CellGridStateWaitingForInput(cellGrid);
    30.   }
    31.  
    32.   public override void CleanUp(CellGrid cellGrid) {
    33.     inAttackRange.ForEach(u => {
    34.       if (u != null) {
    35.         u.UnMark();
    36.       }
    37.     });
    38.   }
    39.  
    40.   public override bool CanPerform(CellGrid cellGrid) {
    41.     var enemyUnits = cellGrid.GetEnemyUnits(cellGrid.CurrentPlayer);
    42.     inAttackRange = enemyUnits.Where(u => UnitReference.IsUnitAttackable(u, UnitReference.Cell)).ToList();
    43.  
    44.     return UnitReference.ActionPoints > 0 && inAttackRange.Count > 0;
    45.   }
    46. }
    47.  
    What happens here is as follows:
    • Display method highlights units that can be attacked
    • OnUnitClicked and OnCellClicked methods handle human interaction
    • Act method verifies if selected action is valid and executes the attack

    A few issues to consider here:
    • At this point it occured to me that Abilities should be self-contained and that Unit class itself is redundant. A Unit could just consist of Abilities and perhaps some data container scripts to store some stats. In the implementation above there are references to fields and methods from Unit though - IsUnitAttackable, AttackHandler among others. Initially I tried extracting these methods along with fields like AttackFactor to AttackAbility. On the other hand, what if I wanted to add skill like charge attack? It would probably also use IsUnitAttackable, Attackhandler and AttackFactor. Should I reference methods from AttackAbility? Make ChargeAbility inherit from AttackAbility? I didn't like any of these options. Finally I came up with IAttackImpl interface that contained all the necesary methods. I liked it better because Unit could implement it, what would make all the previous implementation that users have compatible with the new version, and from now on attack implementation could be decoupled from Unit. I went almost all the way with this idea but in the end I couldn't get it to work.
    As it is now, both AttackAbility and MoveAbility are still referencing methods and fields on Unit. I feel like the ability feature is halfway there and still have more potential. I would be interested to hear your thoughts regarding this issue.​
    • Secondly, how about merging Ability with AIAction (have all the methods from AIAction moved to Ability)? Would you want to have multiple AIActions for a single Ability (for example, a separate action to move towards the enemy to attack, and another for running away when HP is low). On the one hand it makes sense to define how ability is used by the AI in the Ability itself and it wold simplify unit setup process (less components to attach), on the other hand it would make Ability class a bit cluttered. I coded AIAction before I came up with abilities, maybe if it was the other way around I would put the code there from the beginning.
    In the screenshot you can see area of effect attack skill - pretty easy to implement with the new system.
     

    Attached Files:

    • aoe.png
      aoe.png
      File size:
      101.7 KB
      Views:
      9
    aweha likes this.
  28. michal-zetkowski

    michal-zetkowski

    Joined:
    Mar 11, 2015
    Posts:
    163
    Reworked Buff system - I feel like buff system was underused this far, most of users could even be unaware of its existence. Probably my fault for not promoting it enough. I'll try to provide more examples of its usage in the demo scenes. Anyway, it turned out that buffs work nicely with the ability system. I reworked them a bit:
    • They are scriptable objects now
    Code (CSharp):
    1. public abstract class Buff: ScriptableObject
    2.   public int Duration;
    3.  
    4.   public abstract void Apply(Unit unit);
    5.   public abstract void Undo(Unit unit);
    6. }
    • And they are handled a little differently in Unit
    Code (CSharp):
    1. private List < (Buff buff, int timeLeft) > Buffs;
    2.  
    3. public virtual void OnTurnStart() {
    4.   Buffs.FindAll(b => b.timeLeft == 0).ForEach(b => {
    5.     b.buff.Undo(this);
    6.   });
    7.   Buffs.RemoveAll(b => b.timeLeft == 0);
    8.  
    9. }
    10.  
    11. public virtual void OnTurnEnd() {
    12.   for (int i = 0; i < Buffs.Count; i++) {
    13.     (Buff buff, int timeLeft) = Buffs[i];
    14.     Buffs[i] = (buff, timeLeft - 1);
    15.   }
    16. }
     
    aweha likes this.
  29. michal-zetkowski

    michal-zetkowski

    Joined:
    Mar 11, 2015
    Posts:
    163
    Grid rotation - This caused some issues over the years and I would like to finally solve it. In the new version:
    • 2D maps will be generated along XY axis
    • 3D maps will be generated along XZ axis with Y axis considered UP
    To the best of my knowledge, this is how it's supposed to be. Please correct me if I'm wrong.
     

    Attached Files:

    JAMiller, aweha and Crowstone_Games like this.
  30. michal-zetkowski

    michal-zetkowski

    Joined:
    Mar 11, 2015
    Posts:
    163
    Unit / Cell highlighting - One of the most annoying aspects of the framework for me was implementing MarkAs... methods over and over again. More so, because I usually go for the same implementation - either colour the material or the sprite. The solution was to extract these methods to a separate class so they can be reused. I introduced a UnitHighlighter class that is basically a wrapper for a function that applies the effect, and UnitHighlighterAggregator class that applies functions to specific MarkAs... method.

    Code (CSharp):
    1. public abstract class UnitHighlighter : MonoBehaviour
    2. {
    3.     public abstract void Apply(Unit unit, Unit otherUnit);
    4. }
    Code (CSharp):
    1. public class UnitHighlighterAggregator: MonoBehaviour {
    2.   public List < UnitHighlighter > MarkAsAttackingFn;
    3.   public List < UnitHighlighter > MarkAsDefendingFn;
    4.   public List < UnitHighlighter > MarkAsSelectedFn;
    5.   public List < UnitHighlighter > MarkAsFriendlyFn;
    6.   public List < UnitHighlighter > MarkAsFinishedFn;
    7.   public List < UnitHighlighter > MarkAsDestroyedFn;
    8.   public List < UnitHighlighter > MarkAsReachableEnemyFn;
    9.   public List < UnitHighlighter > UnMarkFn;
    10. }
    Here is a sample usage of this system in Unit

    Code (CSharp):
    1. public virtual void MarkAsFriendly() {
    2.   if (UnitHighlighterAggregator != null) {
    3.     UnitHighlighterAggregator.MarkAsFriendlyFn?.ForEach(o => o.Apply(this, null));
    4.   }
    5. }
    And sample implementation

    Code (CSharp):
    1. public class RendererHighlighter: UnitHighlighter {
    2.   public Renderer Renderer;
    3.   public Color Color;
    4.   public override void Apply(Unit unit, Unit otherUnit) {
    5.     Renderer.material.color = Color;
    6.   }
    7. }
    The issues here are as follows:
    • Apply method needs to take two parameters to handle MarkAsAttacking and MarkAsDefending methods (the second Unit parameter indicates defending / attacking unit). Having another class to handle these two methods seems wrong, but I don't like having unused parameter either. Any suggestions?
    • This setup doesn't work very well with prefab variants and Unit class inheritance. In such case all the UnitHighlighter components need to be set up separately for each prefab variant. This is a deal-breaker because it is tedious compared to just coding it once in the parent class. I feel like it is another indication that getting rid of "central" Unit class would be benefitial.
    I'm not sure if I should keep it, but on the other hand, with the MarkAs... methods on Unit being virtual, I guess it doesn't hurt? All of the current implementations will work anyway. Let me know what you think.
     
  31. Crowstone_Games

    Crowstone_Games

    Joined:
    Apr 15, 2013
    Posts:
    4
    Nice! :) That sounds right and would be great! When working in 3D it gets quite confusing correcting rotations.
     
    Last edited: May 23, 2021
  32. Thundar

    Thundar

    Joined:
    Nov 16, 2012
    Posts:
    1
    Hey, just wanted to say thanks. Always wanted to make a game with combat like the old heroes of might & magic games and this framework has allowed me to successfully re-create a basic version of it.

    Unfortunately I've managed to turn the code into a bit of a frankenstein monster along the way by making many changes and additions directly in scripts such as Unit.cs and CellGrid.cs. Was thinking of doing a re-write from the ground up but noticed the planned autumn update for the framework adds a lot of the things I've been crudely trying to implement myself such as initiative based turns, abilities and AI improvements.

    As your solutions seem to be a lot cleaner and more flexible I plan to hold off on re-writing my code for now and focus on other areas of the game in the meantime. Looking forward to the update, keep up the good work!

    Battle.png
     
    aweha and michal-zetkowski like this.
  33. michal-zetkowski

    michal-zetkowski

    Joined:
    Mar 11, 2015
    Posts:
    163
    Wow, this looks really good! I'm currently working on new demo scenes to showcase all the new features. One of the scenes will be HOMM-style, but it pales compared to yours :)
     
  34. aweha

    aweha

    Joined:
    Aug 3, 2015
    Posts:
    4
    Hi @michal-zetkowski , excited to hear that an update is coming!
    Would like to check whether it will still be usable with the Hex Gamepad Controller asset after the update?

    Thank you in advance
     
  35. Reds

    Reds

    Joined:
    Nov 3, 2012
    Posts:
    1
    First, I just want to say that this asset is fantastic. I'm using it for a card-driven roguelike, and getting it to interface nicely with the deck component has been a treat.

    I have, however, hit one snag - [edit] that I have solved.

    Trying to get a unit to spawn using my own code I'd somehow bypassed the Initialise call, and that meant the UnitState wasn't being set. Which is hilariously obvious once you walk away from the screen for a few hours.

    @Liberation85 Hopefully your issue was solved, but just in case you're anything like me, make sure that this line is getting used _somewhere_ accessible before the unit attempts to do anything.

    UnitState = new UnitStateNormal(this);

    Once again - great asset. I'm really enjoying my time with it.
     
    Last edited: Jun 5, 2021
    michal-zetkowski and aweha like this.
  36. Billbot

    Billbot

    Joined:
    Feb 25, 2013
    Posts:
    5
    Hello Friends,
    I am looking to prototype a "hoplite" style game using the turn based strategy framework. I am wondering if this asset would be good for my purpose.
    Thank you!
     
  37. michal-zetkowski

    michal-zetkowski

    Joined:
    Mar 11, 2015
    Posts:
    163
    Sure, I'll try it out :)
     
    aweha likes this.
  38. michal-zetkowski

    michal-zetkowski

    Joined:
    Mar 11, 2015
    Posts:
    163
    I downloaded the game and played through the tutorial and a couple of levels. This is definately doable. A few things to note:
    • The pacing of the game is a little different - the unit can perform one action (move one tile, jump, thrust, push) and then the turn automatically ends. This could be done by giving the unit a single movement point and make other skills use a common pool of action points.
    • The unit attacks automatically while moving or jumping. I've been wondering how to implement it and i think it should be coded in the move and jump ability.
    • Finally, the abilities. Recently I posted here about the update that is coming to the framework. One of the new features is a new Ability System. Jumping, thrusting and pushing would be a great use cases for this system. It is still doable in the current version, but the version will make it easier.
    Let me know if you have more questions
     
  39. Billbot

    Billbot

    Joined:
    Feb 25, 2013
    Posts:
    5
    Thank you for your thorough answer! I'm going to give it a try.

    https://play.google.com/store/apps/details?id=com.macecrystal.and.vikingtales&hl=en_US&gl=US

    This game is very similar. I like the style more on the Viking one.
     
  40. aweha

    aweha

    Joined:
    Aug 3, 2015
    Posts:
    4
    Thanks! I believe it should still be working though, but just need to double check with you.
     
  41. BehindScheduleDev

    BehindScheduleDev

    Joined:
    Apr 29, 2021
    Posts:
    1
    Two questions - your framework looks really good and I'm considering picking it up but I would like to know:
    1) I'm planning to implement a solution where you select a unit, select a target and then choose from a list of weapons to fire at that target, with the ability to fire multiple weapons in a turn. Is that something that can easily be added using your framework?
    2) How much rework will be needed after you add the new changes you plan for this fall? Is it worth starting using the engine now, or would it be better just to wait till the update?
     
    aweha likes this.
  42. michal-zetkowski

    michal-zetkowski

    Joined:
    Mar 11, 2015
    Posts:
    163
    Hey,

    In the current version you would code it in CellGridStateUnitSelected, and if you wanted to allow AI to use it, then you would also need to kind of hardcode it in NaiveAiPlayer script. Certainly doable, but you need to modify existing code and it would not be pretty.

    The new version will make it a lot cleaner. Transition will not be "automatic", but pretty straightfoward (I know it might be easy for me to say). You extract your code from CellGridStateUnitSelected into new Ability component and define in your custom AI action how the AI will use the ability.

    Let me know if you have more questions
     
  43. Liberation85

    Liberation85

    Joined:
    Apr 16, 2019
    Posts:
    51
    Any chance the new AI scripting could be dropped into an existing project?

    The new changes sound awesome, but a rewrite is not sounding so good :)

    @Reds Cheers! I did get it sorted in the end.
     
  44. noki3310

    noki3310

    Joined:
    May 26, 2021
    Posts:
    1
    I am a person who bought Turn Based Strategy Framework and need help with a problem


    Sorry for my poor English but I really need help please


    I follow all the objects in this example, hoping to achieve the same effect as the example

    But in the end, I found a problem. My character cannot move but can directly attack another character.


    The movable range is not displayed even after clicking


    But the magic is that once I replaced the cellgrid I made with the sample cellgrud, it returned to normal again. But I can’t see the difference between what I did and the example
     
  45. michal-zetkowski

    michal-zetkowski

    Joined:
    Mar 11, 2015
    Posts:
    163
    You could use the new AIPlayer script, cell evaluators could be used with minor changes, and finally you would need to code basic AIActions yourself - not too complicated IMO :)
     
unityunity