Search Unity

[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:
    65

    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:
    282
    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:
    282
    @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:
    7
    Hey, what sizes are the tiles and unit sprites used in the example maps?
     
  7. Panhypersebastos

    Panhypersebastos

    Joined:
    Feb 21, 2017
    Posts:
    44
    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:
    282
    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:
    282
    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:
    78
    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:
    15
    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:
    78
    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:
    282
    @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:
    65
    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:
    282
    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:
    282
    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:
    3
    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:
    282
    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:
    3
    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:
    282
    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:
    282
    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.
     
  24. michal-zetkowski

    michal-zetkowski

    Joined:
    Mar 11, 2015
    Posts:
    282
    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:
    282
    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:
    282
    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:
    282
    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:
      355
    aweha likes this.
  28. michal-zetkowski

    michal-zetkowski

    Joined:
    Mar 11, 2015
    Posts:
    282
    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:
    282
    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:
    282
    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:
    5
    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:
    6
    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
     
    LordDooms, aweha and michal-zetkowski like this.
  33. michal-zetkowski

    michal-zetkowski

    Joined:
    Mar 11, 2015
    Posts:
    282
    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:
    3
    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:
    282
    Sure, I'll try it out :)
     
    aweha likes this.
  38. michal-zetkowski

    michal-zetkowski

    Joined:
    Mar 11, 2015
    Posts:
    282
    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. michal-zetkowski

    michal-zetkowski

    Joined:
    Mar 11, 2015
    Posts:
    282
    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
     
  42. Liberation85

    Liberation85

    Joined:
    Apr 16, 2019
    Posts:
    65
    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.
     
  43. 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
     
  44. michal-zetkowski

    michal-zetkowski

    Joined:
    Mar 11, 2015
    Posts:
    282
    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 :)
     
  45. amiyagisp

    amiyagisp

    Joined:
    Sep 30, 2020
    Posts:
    1
    Hi, is there a way to implement foreshadowing of the enemies actions like Into the Breach?
     
  46. michal-zetkowski

    michal-zetkowski

    Joined:
    Mar 11, 2015
    Posts:
    282
    Hey, I will check out this feature and edit this post over the weekend, sorry for delayed reply.

    EDIT:

    @amiyagisp Ok, I checked out the game. As far as I understand, enemy units decide on their next move at the end of their turn and display some kind of indication, right?

    I would implement it by adding methods like "DecideNextMove" and "DisplayNextMove" methods to Unit. You would call the methods from AI script when the units are done making their moves for a given turn.
     
    Last edited: Jun 27, 2021
    amiyagisp likes this.
  47. jcjc2k10

    jcjc2k10

    Joined:
    May 2, 2017
    Posts:
    2
    Hello I am starting a 2.5d isometric project.. I have some tileset assets and was wondering if the engine supported 2d isometric tiles and if so if you had any suggestions on how to implement it.
     
  48. Neil2TheKing

    Neil2TheKing

    Joined:
    Sep 14, 2017
    Posts:
    15
    This took longer than I expected. My code has drifted from the original TBS Framework, please let me know if you run into issues and I’ll modify this post accordingly.

    This tutorial goes over how to set up online multiplayer for the Turn Based Strategy Framework using PUN 2. This tutorial uses a Main Menu scene to help set up online multiplayer. Please create a Main Menu scene before starting this tutorial.

    This tutorial is split into two parts. The first part sets up an online multiplayer room and lobby while the second part adds code to a TBSF scene to allow for online play. If this is your first time setting up online multiplayer with PUN 2 please read both parts 1 and 2. If you have experience with PUN 2 feel free to skip ahead to Part 2 to find the code to add to your multiplayer game scene.

    Part 1
    PUN 2 (Photon Unity Networking 2) is an online networking tool that lets you set up online multiplayer for Unity games. It lets you network up to 20 players at the same time for free with the option to purchase space for more players as needed. To use PUN 2 for your Unity Game you need to create a Photon appID and download Photon from the Unity Asset Store. Let’s start by creating the Photon Account.

    To generate a Photon AppID you first need a Photon account. To create a Photon Account go to dashboard.photonengine.com and click the”Create One” button. follow the instructions to set up your account and once It’s set up sign in. From your account homepage click the “Create New App” button. This will open up a new screen. In this screen set your Photon Type to PUN, name your application, and give it a description to help you remember it. Create the app. You should see the newly created app on your dashboard. Inside the app’s summary box you should see a value called App ID. Copy this id, we will need it to connect your Unity Scene to the appropriate server.

    Once you’ve created your App ID open your game in Unity and go to the Unity Asset Store. Find PUN 2 and start downloading it. When you download PUN 2 it will ask you for your App ID. Enter the App ID you created above to connect your Unity game to the correct Photon server. Finish downloading PUN 2. Once it’s downloaded you will see it in your Asset Hierarchy under the name Photon.

    Open the Photon Asset, open the PhotonUnityNetworking folder, and then open the Resources folder. Within the Resources folder click on the object called “PhotonServerSettings”. In the inspector find the parameter called ”Fixed Region” within “PhotonServerSettings”. Set the value of Fixed Region” to “usw” , without the quotes. This will set a fixed region for your game which makes sure a player in Paris can play with a friend in California.

    Next connect players to the online server. To do this we will need a second scene separate from the one for gameplay. I recommend creating a Main Menu scene to handle this. If you have not created a Main Menu Scene Please do so now. This section is tricky to do without a menu screen to let things load.

    In your Main Menu scene create a new empty GameObject to handle online interactions. Attach a Photon View component to this GameObject and create a new script called OnlineMultiplayerSetup and attach it to the GameObject. Paste the following code into the OnlineMultiplayerSetup script:

    Code (CSharp):
    1. public class OnlineMultiplayerSetup : MonoBehaviourPunCallbacks
    2. {
    3.     public override void OnJoinedRoom()
    4.     {
    5.         if (PhotonNetwork.CurrentRoom.PlayerCount == PhotonNetwork.CurrentRoom.MaxPlayers)
    6.         {
    7.             PhotonNetwork.CurrentRoom.IsOpen = false;
    8.             photonView.RPC("RPC_StartGame", RpcTarget.All);
    9.  
    10.         }
    11.     }
    12.  
    13.     [PunRPC]
    14.     public void RPC_StartGame()
    15.     {
    16.        ExitGames.Client.Photon.Hashtable photonHash = new ExitGames.Client.Photon.Hashtable();
    17.         int startingRandomState = UnityEngine.Random.Range(0, 3603600);
    18.         photonHash.Add("RandomNumber", startingRandomState);
    19.  
    20.         if (PhotonNetwork.IsMasterClient)
    21.         {
    22.             PhotonNetwork.LoadLevel("Online");
    23.         }
    24.     }
    25.  
    26.     public void JoinOnlineServer()
    27.     {
    28.         PhotonNetwork.ConnectUsingSettings();
    29.     }
    30.  
    31.     public override void OnConnectedToMaster()
    32.     {
    33.         onlineCharacters = player1Characters;
    34.         PhotonNetwork.AutomaticallySyncScene = true;
    35.         PhotonNetwork.JoinLobby();
    36.     }
    37.  
    38.     public void StartGameWithFriend()
    39.     {
    40.         if (PhotonNetwork.CurrentRoom != null)
    41.         {
    42.             PhotonNetwork.LeaveRoom();
    43.         }
    44.         else
    45.         {
    46.             string password = GameObject.Find("Password").GetComponent<TMP_InputField>().text;
    47.             Regex rgx = new Regex("[^a-zA-Z0-9]");
    48.             string revisedPassword = Regex.Replace(password, "[^A-Za-z0-9]", "");
    49.             RoomOptions roomOptions = new RoomOptions();
    50.             roomOptions.IsVisible = false;
    51.             roomOptions.MaxPlayers = 2;
    52.             PhotonNetwork.JoinOrCreateRoom(revisedPassword, roomOptions, TypedLobby.Default);
    53.             //GameObject.Find("Password").GetComponent<TMP_InputField>().text = revisedPassword;
    54.         }
    55.     }
    56.  
    57.     public void Disconnect()
    58.     {
    59.         PhotonNetwork.Disconnect();
    60.     }
    61.  
    62.     public void PlayWithAnyOne()
    63.     {
    64.         if (PhotonNetwork.CurrentRoom != null)
    65.         {
    66.             PhotonNetwork.LeaveRoom();
    67.         }
    68.         else
    69.         {
    70.             PhotonNetwork.JoinRandomRoom();
    71.         }
    72.  
    73.     }
    74.     public override void OnJoinRandomFailed(short returnCode, string message)
    75.     {
    76.         CreateRandomRoom();
    77.     }
    78.  
    79.     private void CreateRandomRoom()
    80.     {
    81.         string x = UnityEngine.Random.Range(0, 1000000).ToString("D6");
    82.         RoomOptions roomops = new RoomOptions() { IsVisible = true, IsOpen = true, MaxPlayers = 2 };
    83.         PhotonNetwork.CreateRoom("Room" + x, roomops);
    84.     }
    85.     public override void OnCreateRoomFailed(short returnCode, string message)
    86.     {
    87.         CreateRandomRoom();
    88.     }
    89. }
    90.  
    Notice that the script inherits from MonoBehaviorPunCallbacks. This is important. Make sure your script does as well. The above script has everything you need to get two players connected to the photon network and to start a multiplayer game for the two players. You will need to trigger a couple methods with button presses to connect to the online server and to start an online game. Let’s set those up now.

    In your Main Menu Scene, Create a “Play Online” button. Attach the OnlineMultiplayerSetup.JoinOnlineServer() function to the button. This function connects the player’s computer to the online lobby. The function takes a couple seconds to finish running. Attach a second function to the “Play Online” button that opens a new menu that looks like the one below:

    Once players are connected to the online server and in the same lobby this screen will let them play an online multiplayer game with friends or with anyone online. To let players play with anyone attach the PlayWithAnyOne() function to the “Play With Anyone” button. When a player clicks this button the Photon Server will find another player who also clicked Play With Anyone, match them together, and start the online game for both players. To let players play with their friends attach the StartGameWithFriends() function to the Play With Friend Button. Instruct players to enter a password into the textbox and then click the Play With Friend Button. Once both players enter the same password into the textbox and click the button the Photon Server will find both players, pair them into a room, and start the game for them.

    Please note you may need to change the following to get the OnlineMultiplayerSetup script to get it working properly with your game.

    1. In the above script the RPC_StartGame method has this line of code: PhotonNetwork.LoadLevel("Online"). You may need to change “Online” to the name of your online multiplayer game scene. The LoadLevel function opens a new scene from the game’s build for all players in the same Photon Room. It identifies the scene by its name. In my game I called the scene with online multiplayer “Online”. If you called your online multiplayer scene something different replace “Online” with your scene’s name.

    2. The StartGameWithFriends() method pairs two friends together into the same room when they enter the same password into a textbox. The code assumes the gameobject with the textbox is called “Password” and it is using a TMP_InputField component for the textbox. If your textbox works differently or is named differently replace this line of code with something more appropriate: string password = GameObject.Find("Password").GetComponent<TMP_InputField>().text;

    3. The OnlineMultiplayerSetup() code assumes the game is 2 player. If you want to allow more players change the RoomOptions.MaxPlayers variable equal to your desired number of players. RoomOptions.MaxPlayers can be found in the CreateRandomRoom() and the StartGameWithFriend() Methods.

    The above steps will get players into the same Photon Network Room and will launch a new online multiplayer game for them. In the next section we will add code to a TBS Framework game scene to sync actions across all computers in the room.


    Part 2
    This part of the tutorial is still under development. The code from my game has drifted somewhat from the original TBS Framework, I am still lining things back up. As a sneak peak here are the code bits I added to a standard TBS Framework scene to enable online multiplayer.



    CellGridOnline:

    Code (CSharp):
    1. public class CellGridOnline : CellGrid
    2. {
    3.    
    4.     protected override void SetRandomSeed()
    5.     {
    6.         int rngSeed = (int)PhotonNetwork.CurrentRoom.CustomProperties["RandomNumber"];
    7.         Debug.Log("RNG Number is " + rngSeed + "!");
    8.         dejavu = new System.Random(rngSeed);
    9.     }
    10.     public void Disconnect()
    11.     {
    12.         PhotonNetwork.Disconnect();
    13.     }
    14.  
    15.     [PunRPC]
    16.     public void RPC_EndTurn()
    17.     {
    18.         EndTurn();
    19.     }
    20.  
    21.     public void OnlineEndTurn()
    22.     {
    23.         if (CurrentPlayer.GetComponent<PhotonView>().IsMine)
    24.         {
    25.             gameObject.GetComponent<PhotonView>().RPC("RPC_EndTurn", RpcTarget.OthersBuffered);
    26.             base.EndTurn();
    27.         }
    28.     }
    29.     public override void MovementFinished(Unit actor, Cell startingSpace, Cell DestinationSpace, List<Cell> path,int[] rngNumbers=null)
    30.     {
    31.         if (CurrentPlayer.GetComponent<PhotonView>().IsMine)
    32.         {
    33.             int[] originCoord = new int[2];
    34.             originCoord[0] = (int)startingSpace.OffsetCoord.x;
    35.             originCoord[1] = (int)startingSpace.OffsetCoord.y;
    36.             int[] targetCoord = new int[2];
    37.             targetCoord[0] = (int)DestinationSpace.OffsetCoord.x;
    38.             targetCoord[1] = (int)DestinationSpace.OffsetCoord.y;
    39.             int[] xPath = new int[path.Count];
    40.             int[] yPath = new int[path.Count];
    41.             int pathCounter = 0;
    42.             foreach (Cell c in path)
    43.             {
    44.                 if (pathCounter > xPath.Length)
    45.                 {
    46.                     continue;
    47.                 }
    48.                 xPath[pathCounter] = (int)c.OffsetCoord.x;
    49.                 yPath[pathCounter] = (int)c.OffsetCoord.y;
    50.                 pathCounter++;
    51.             }
    52.  
    53.             gameObject.GetComponent<PhotonView>().RPC("RPC_MirrorMovement", RpcTarget.OthersBuffered, originCoord as object, targetCoord as object, xPath as object, yPath as object, rngNumbers as object);
    54.  
    55.         }
    56.         base.MovementFinished(actor, startingSpace, DestinationSpace, path);
    57.     }
    58.     public override void ActionFinished(Cell originSpace, Unit actor, Cell targetSpace, string actionName, int[] rngNumber)
    59.     {
    60.         if (CurrentPlayer.GetComponent<PhotonView>().IsMine)
    61.         {
    62.             int[] originCoord = new int[2];
    63.             originCoord[0] = (int)originSpace.OffsetCoord.x;
    64.             originCoord[1] = (int)originSpace.OffsetCoord.y;
    65.             int[] targetCoord = new int[2];
    66.             targetCoord[0] = (int)targetSpace.OffsetCoord.x;
    67.             targetCoord[1] = (int)targetSpace.OffsetCoord.y;
    68.  
    69.             gameObject.GetComponent<PhotonView>().RPC("RPC_MirrorAction",RpcTarget.OthersBuffered,originCoord as object,targetCoord as object,rngNumber as object);
    70.         }
    71.         base.ActionFinished(originSpace, actor, targetSpace, actionName, rngNumber);
    72.     }
    73.  
    74.     [PunRPC]
    75.     public void RPC_MirrorMovement(int[] originCoord,int[] destinationCoords, int[] pathXCoord,int[] pathYCoord, int[]rngNumbers)
    76.     {
    77.        
    78.         Cell originSpace = Cells.Find(c => (int)c.OffsetCoord.x == originCoord[0] && c.OffsetCoord.y == originCoord[1]);
    79.         Cell targetSpace = Cells.Find(c => (int)c.OffsetCoord.x == destinationCoords[0] && c.OffsetCoord.y == destinationCoords[1]);
    80.         List<Cell> path = new List<Cell>();
    81.         Cell cellinPath;
    82.         for (int i=0; i < pathYCoord.Length; i++)
    83.         {
    84.             cellinPath = Cells.Find(c => (int)c.OffsetCoord.x == pathXCoord[i] && (int)c.OffsetCoord.y == pathYCoord[i]);
    85.             if (cellinPath == null)
    86.             {
    87.                 continue;
    88.             }
    89.             Debug.Log(pathXCoord[i] + ", " + pathYCoord[i]);
    90.             path.Add(cellinPath);
    91.         }
    92.         path.Reverse();
    93.         if (originSpace.CurrentUnit != null)
    94.         {
    95.             originSpace.CurrentUnit.Move(targetSpace, path, this, rngNumbers);
    96.             CheckIfUnitsDefeated();
    97.         }
    98.  
    99.     }
    100.  
    101.     [PunRPC]
    102.     public void RPC_SkipAction(int skippingUnitXCoord,int skippingUnitYCoord)
    103.     {
    104.         Cell originSpace=Cells.Find(c => (int)c.OffsetCoord.x == skippingUnitXCoord && c.OffsetCoord.y == skippingUnitYCoord);
    105.         originSpace.CurrentUnit.ActionSkipped(this);
    106.         originSpace.CurrentUnit.SetState(new UnitStateMarkedAsFinished(originSpace.CurrentUnit));
    107.         this.CellGridState = new CellGridStateWaitingForInput(this);
    108.         if (PlayerTotalActions <= 0)
    109.         {
    110.             EndTurn();
    111.         }
    112.     }
    113.  
    114.     public override void ActionSkipped(Cell skippersSpace)
    115.     {
    116.         if (CurrentPlayer.GetComponent<PhotonView>().IsMine)
    117.         {
    118.             int x = (int)skippersSpace.OffsetCoord.x;
    119.             int y = (int)skippersSpace.OffsetCoord.y;
    120.             gameObject.GetComponent<PhotonView>().RPC("RPC_SkipAction", RpcTarget.OthersBuffered, x, y);
    121.         }
    122.         base.ActionSkipped(skippersSpace);
    123.     }
    124.  
    125.     [PunRPC]
    126.     public void RPC_MirrorAction(int[] originCoord, int[] destinationCoords, int[] rngNumbers)
    127.     {
    128.         Cell originSpace = Cells.Find(c => (int)c.OffsetCoord.x == originCoord[0] && c.OffsetCoord.y == originCoord[1]);
    129.         Cell targetSpace = Cells.Find(c => (int)c.OffsetCoord.x == destinationCoords[0] && c.OffsetCoord.y == destinationCoords[1]);
    130.         Unit actingUnit = originSpace.CurrentUnit;
    131.         if (originSpace.CurrentUnit != null)
    132.         {
    133.             originSpace.CurrentUnit.PerformAction(originSpace.CurrentUnit, targetSpace, this, rngNumbers);
    134.             actingUnit.SetState(new UnitStateMarkedAsFinished(actingUnit));
    135.             this.CellGridState = new CellGridStateWaitingForInput(this);
    136.             if (this.PlayerTotalActions <= 0)
    137.             {
    138.                 this.EndTurn();
    139.             }
    140.         }
    141.     }
    142.  
    143.     protected override void OnCellClicked(object sender, EventArgs e)
    144.     {
    145.         PhotonView myPV= gameObject.GetComponent<PhotonView>();
    146.         PhotonView cpPV = CurrentPlayer.GetComponent<PhotonView>();
    147.         if (cpPV == null)
    148.         {
    149.             return;
    150.         }
    151.         if (!cpPV.IsMine)
    152.         {
    153.             return;
    154.         }
    155.         else
    156.         {
    157.             base.OnCellClicked(sender, e);
    158.         }
    159.        
    160.     }
    161.  
    162.    
    163. }
    164.  
    165.  
    LoadOnlinePlayers:

    Code (CSharp):
    1. public class LoadOnlinePlayers : MonoBehaviourPun
    2. {
    3.     protected int playersEntered = 0;
    4.     public void Awake()
    5.     {
    6.         photonView.RPC("RPC_LoadPlayers", RpcTarget.AllBuffered);
    7.     }
    8.  
    9.     [PunRPC]
    10.     public void RPC_LoadPlayers()
    11.     {
    12.         int rngSeed = (int)System.DateTime.Now.Ticks;
    13.         Debug.Log("trying to load player");
    14.         GameObject newplayer;
    15.         if (PhotonNetwork.IsMasterClient)
    16.         {
    17.             newplayer=PhotonNetwork.Instantiate("PlayerOne", Vector3.zero, Quaternion.identity);
    18.         }
    19.         else
    20.         {
    21.             newplayer = PhotonNetwork.Instantiate("PlayerTwo", Vector3.zero, Quaternion.identity);
    22.         }
    23.         photonView.RPC("RPC_Player_Added", RpcTarget.AllBuffered,rngSeed);
    24.  
    25.     }
    26.     [PunRPC]
    27.     public void RPC_Player_Added(int rngSeed)
    28.     {
    29.         Debug.Log("Player_Added");
    30.         playersEntered++;
    31.         if (playersEntered == 2)
    32.         {
    33.             Debug.Log(rngSeed);
    34.             Random.InitState(rngSeed);
    35.             CellGridOnline cellgrid = GameObject.Find("CellGrid").GetComponent<CellGridOnline>();
    36.             GameObject p1 = GameObject.Find("PlayerOne(Clone)");
    37.             GameObject p2 = GameObject.Find("PlayerTwo(Clone)");
    38.             if (p1 != null)
    39.             {
    40.                 p1.transform.SetParent(this.transform);
    41.                 p1.gameObject.name = "Player_0";
    42.                 cellgrid.AddPlayer(p1.GetComponent<Player>());
    43.             }
    44.             if (p2 != null)
    45.             {
    46.                 p2.transform.SetParent(this.transform);
    47.                 p2.gameObject.name = "Player_1";
    48.                 cellgrid.AddPlayer(p2.GetComponent<Player>());
    49.  
    50.             }
    51.             GameObject cellgridObject = GameObject.Find("CellGrid");
    52.             if (cellgridObject != null)
    53.             {
    54.                 cellgridObject.GetComponent<CellGridOnline>().SetPlayerNumber(0);
    55.             }
    56.  
    57.         }
    58.     }
    59.  
    60. }
    61.  
    62.  
    63.  
    The CellGridOnline replaces the standard CellGrid script and communicates when a player either moves a unit, attacks with a unit, or clicks a button. It also includes some code that makes sure players can only move characters during their own turn and ensures both players are using the same random state if your game uses rng. The LoadOnlinePlayers script loads Player GameObjects from the Photon Network. This helps CellGridOnline know which player are controlled by which computer, ensuring players only move on their turn.



    To set up online multiplayer with the above code do the following

    1. Replace CellGrid with CellGridOnline. Initialize all public parameters on CellGridOnline as appropriate. You may need to change the name of several functions like MoveMentFinished() and ActionFinished() to their corresponding functions in the TBS Framework.

    2. You will need to add a PhotonView Component to the CellGrid and Players GameObjects.Give both components unique ViewID’s so the network can tell them apart

    3. LoadOnlinePlayers creates new GameObjects using the Photon Network. This connects the player GameObjects with their corresponding computers. For this to happen properly you must delete the original Player GameObjects inside Players. You also need to create the GameObjects that PhotonNetwork is supposed to load. To do this duplicate a HumanPlayer prefab from the TBS Framework folder. Move it to Photon/Resources, add a PhotonView component to it. Change this GameObject’s name to PlayerOne and set it’s Player Number to 0. Repeat this process again for a second player, this time renaming the GameObject PlayerTwo and setting it’s player number to 1.

    These steps should set up online multiplayer in the framework. The steps are definitely rough. I will tinker with the instructions and my code to make it more reusable. Let me know if you get stuck on a particular part
     
  49. michal-zetkowski

    michal-zetkowski

    Joined:
    Mar 11, 2015
    Posts:
    282
    Wow, this is great, thank you for your effort :D. I'll try to reproduce it as soon as I have some time. Do you have any tips on how I could redesign the framework to facilitate the process?
     
    stoven91 likes this.
  50. michal-zetkowski

    michal-zetkowski

    Joined:
    Mar 11, 2015
    Posts:
    282
    Hey, to get isometric tiles to work you need to modify GridGenrator script, so they place isometric tiles correctly. I did a proof of concept for isometric tiles some time ago, I attach a screenshot and GridGenerator script that I used to generate the grid. As you can see, the grid is generated fine, but unit placement is a bit off. Other than that, the map is playable. Make sure to set CellDinensions on your tile prefab correctly, in my case i set it to Vector3(Mathf.Sqrt(2), 0.8f, 1). Let me know if you have issues implementing it.
     

    Attached Files:

    Deleted User and jcjc2k10 like this.