Search Unity

Board Game Tile Specific Rules

Discussion in 'Scripting' started by LukeDodds, Feb 7, 2015.

  1. LukeDodds

    LukeDodds

    Joined:
    Oct 24, 2014
    Posts:
    46
    Trying to make a board game with a monopoly vibe.
    So if a player lands of a specific square, they have to go back a number of spaces and then their turn ends.
    Not sure how I go about doing that though.
    Currently a dice is rolled and then for example if the number is 3 then all 3 infront of the player is highlighted and this is what happens (in the gamemanager) when a highlighted space is clicked:

    Code (csharp):
    1.  
    2. PlayerOneObject.GetComponent<PlayerOne> ().currentSpace = space;
    3. PlayerOneObject.transform.position = new Vector3 (space.transform.position.x, space.transform.position.y + PlayerOneObject.GetComponent<PlayerOne> ().yOffset, space.transform.position.z);
    4.  
    So from my specific tile script how can i make the player go back?
    this is what iv tried so far but doesnt work
    Code (csharp):
    1.  
    2. MoveSpace = GameObject.Find ("PlayerOnePiece").GetComponent<PlayerOne> ().currentSpace
    3. PlayerOnePiece = GameObject.Find ("GameManager").GetComponent<Board> ().PlayerOne
    4. PlayerOneObject.GetComponent<PlayerOne> ().currentSpace = space;
    5. PlayerOneObject.transform.position = new Vector3 (space.transform.position.x, space.transform.position.y + PlayerOneObject.GetComponent<PlayerOne> ().yOffset, space.transform.position.z);
    6.  
     
  2. BenZed

    BenZed

    Joined:
    May 29, 2014
    Posts:
    524
    Ooooh fun little Saturday project.

    I would have all of the possible tiles arranged sequentially in an ordered list, where positive indexes match a step forward in game position, using a custom tile class. The tile class would know its index in the list, and its location in world space:

    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections.ObjectModel; //For read only list
    3. using System.Collections.Generic;
    4.  
    5. //Our tile class only keeps track of where the tile is in game space, and it's order in the sequence of
    6. //the game. For this to work, we need to have tiles placed in the scene with this script as a component.
    7. //This class will check for errors made in the scene concerning the naming of tiles.
    8.  
    9. public class Tile : MonoBehaviour {
    10.  
    11.     //Non-Editable list of tiles
    12.     public static ReadOnlyCollection<Tile> InOrder;
    13.  
    14.     //Editable list of tiles. We could have probably used a sorted list
    15.     //or some other collection, but for the basics, a list will do just fine.
    16.     static List<Tile> inOrder = new List<Tile>();
    17.  
    18.     //A flag for error-checking gaps in our tilesInOrder list.
    19.     static bool orderGapCheckComplete = false;
    20.  
    21.     int index;
    22.     public int Index {
    23.         get {
    24.             return index;
    25.         }
    26.     }
    27.  
    28.     void Awake () {
    29.    
    30.         //First we make a reference to the read-only collection of tiles if one doesn't exist already.
    31.         //This is so outside classes can access the list of tiles, but not change it.
    32.         if (InOrder == null) {
    33.             InOrder = inOrder.AsReadOnly();
    34.         }
    35.  
    36.         //There are lots of ways to decide a tiles index. In this case, we're going to take the index
    37.         //by parsing the name of the tile in the heirarchy view. That way, in the editor, you can plainly
    38.         //see the order of the tiles, if there duplicate indexes and change them, if necessary. For organizational
    39.         //sake, all the tiles are under a parent GameObject
    40.         try {
    41.             index = System.Convert.ToInt32 (name);
    42.         } catch {
    43.             throw new UnityException("Tile name must be a number that corresponds with its order on the board, (Eg 1,2,3,4,5)");
    44.         }
    45.  
    46.         //We've got to make sure our list has enough room to hold this index, otherwise it will throw an error
    47.         // +1 because indexes are zero based. If we want to place an item at index 0 (the first possible in the list)
    48.         // we need to ensure the list has the Capacity to store 1 item.
    49.         while (inOrder.Count < index + 1)
    50.             inOrder.Add (null);
    51.  
    52.         //Now that we've got our index and storage, we place this tile in the List. It's important that the spot we've given it
    53.         //isn't taken by another tile. We can't have the players moving to two tiles at once, after all.
    54.         if (inOrder[index] != null)
    55.             throw new UnityException("Two tiles cannot share the same index!");
    56.  
    57.         inOrder[index] = this;
    58.  
    59.     }
    60.  
    61.     void Start() {
    62.         //Start() is called after Awake() is called. We have to make sure there are no gaps in the last. If we have 7 game tiles, and we accidentally deleted
    63.         //the one named '3', players will have no tile to jump to after 2!
    64.  
    65.         if (!orderGapCheckComplete) {
    66.             //We're setting a flag because each tile will make this check otherwise. It will only fail once, so it only needs to be checked once.
    67.             //It doesn't explicitly matter if each tile makes this check, but it's good practice to understand the difference between static
    68.             //and instance variables.
    69.             orderGapCheckComplete = true;
    70.  
    71.             for (int i = 0; i < InOrder.Count; i++) {
    72.                 if (InOrder [i] == null)
    73.                     throw new UnityException("Missing tile at index "+i+"! You can't have any gaps in the list. Add and place a tile with index "+i+" then run the scene again.");
    74.             }
    75.         }
    76.     }
    77.  
    78.     //And voila! That's all we need for our tile class. It doesn't need to know or do anything else.
    79.  
    80. }
    81.  
    You COULD arrange the list using a Game Manager class, but i've just made it so you create the order and position in the editor.

    Each player would be their own custom class that you instantiate for each player. (I notice above you have a class specifically for PlayerOne. Do not do this) The player class would know about the tile it's currently on, and contain functions that allow it to move to one tile or another:

    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections;
    3.  
    4. //The player class knows what tile it's on, and contains functions for moving about tiles.
    5. public class Player : MonoBehaviour {
    6.  
    7.     //In the editor, I've just dragged tile 0 into this slot.
    8.     public Tile currentTile;
    9.  
    10.     //We want to make sure the player's GameObject is physically on the it's current tile.
    11.     //We'll do that in Update()
    12.     void Update () {
    13.         transform.position = Vector3.Lerp (transform.position, currentTile.transform.position, Time.deltaTime * 5f);
    14.     }
    15.  
    16.     //This function could also go in the tile class, but all it does is wrap a given
    17.     //index around in case we try to go to a tile that's above or below the number of tiles we have.
    18.     //In the example scene, if we try to move to index 7, it should put us right back to index 0, completing
    19.     //the circle.
    20.     int ResolveIndex(int input) {
    21.  
    22.         int output = input;
    23.  
    24.         while (output >= Tile.InOrder.Count)
    25.             output -=  Tile.InOrder.Count;
    26.  
    27.         while (output < 0)
    28.             output +=  Tile.InOrder.Count;
    29.  
    30.         return output;
    31.  
    32.     }
    33.  
    34.     //Positive for forward, negative for backward.
    35.     public void MoveForwardOrBackward(int times) {
    36.         int targetIndex = currentTile.Index + times;
    37.         int resolvedIndex = ResolveIndex (targetIndex);
    38.  
    39.         currentTile = Tile.InOrder [resolvedIndex];
    40.     }
    41.  
    42.     //If we want to move a player to a specific tile (like at the beginning of the game)
    43.     //this function will be handy.
    44.     public void MoveToSpecificTile(int index) {
    45.         int resolvedIndex = ResolveIndex (index);
    46.  
    47.         currentTile = Tile.InOrder [resolvedIndex];
    48.     }
    49.  
    50.     //And that is it for our player class!
    51. }
    52.  

    Now, just for fun, lets learn about abstract classes for our rules. See, our Rule class will be a monoBehaviour that we attach to a tile to give it a certain action, like when a player lands on it.

    Since all of our rules will be different, one single class wont do for all for all of them, so first we'll create an abstract class that contains everything that all rules will have in common:

    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections.Generic;
    3.  
    4. //Base class for rule. Holds all of the properties common to all rules.
    5. public abstract class Rule : MonoBehaviour {
    6.    
    7.     public enum Effect {
    8.         Good,
    9.         Bad,
    10.     }
    11.  
    12.     //Our derived rules will either have a positive or negative effect on players.
    13.     //They'll have to explicitly state it, so we'll make it an abstract property.
    14.     public abstract Effect effect { get; }
    15.  
    16.     //The rule behaviours will be placed on tiles, so this property will return the tile the rule is attached to.
    17.     Tile _tile;
    18.     public Tile tile {
    19.         get {
    20.             return _tile;
    21.         }
    22.     }
    23.  
    24.     //Static field that holds all of the players in the game. We dont want our derived
    25.     //rules to be to edit it, so we'll keep it private
    26.     static Player[] allPlayers;
    27.  
    28.     //Handy function we'll want our derived rules to have
    29.     protected Player[] GetPlayersOnTile() {
    30.         var playerList = new List<Player> ();
    31.  
    32.  
    33.         foreach (var player in allPlayers) {
    34.             //If the player's current tile is this tile, and the player is very close to this tile, we'll say it's on this tile. That
    35.             //Way, it wont count if a player is still moving toward this tile from another.
    36.             if (player.currentTile == tile && (player.transform.position - player.currentTile.transform.position).magnitude < 0.05f)
    37.                 playerList.Add (player);
    38.         }
    39.  
    40.         return playerList.ToArray ();
    41.  
    42.     }
    43.  
    44.     //Protected and virtual because we want our derived rules to be able to run Awake methods by overriding them, if necessary.
    45.     protected virtual void Awake() {
    46.  
    47.         _tile = GetComponent<Tile> ();
    48.         if (!_tile)
    49.             throw new UnityException ("Rules must be placed on Tiles!");
    50.  
    51.         //If we havent found all the players yet, we do so now.
    52.         if (allPlayers == null) {
    53.             allPlayers = GameObject.FindObjectsOfType<Player>();
    54.         }
    55.  
    56.         //We'll make the tiles with bad rules colored red, and the tiles with good rules colored green.
    57.         Color effectColor = (effect == Effect.Good) ? Color.green : Color.red;
    58.         tile.renderer.material.color = effectColor;
    59.     }
    60.  
    61.     //And that's it for our rule base class!
    62. }
    63.  
    Next we'll want to create a couple of rules. One good and one bad:
    Code (CSharp):
    1. //We'll say the good rule moves players forward an additional tile.
    2. public class GoodRule : Rule {
    3.  
    4.     public override Effect effect {
    5.         get {
    6.             return Rule.Effect.Good;
    7.         }
    8.     }
    9.  
    10.     void Update() {
    11.         var playersOnTile = GetPlayersOnTile ();
    12.    
    13.         foreach (var player in playersOnTile) {
    14.             player.MoveForwardOrBackward(1);
    15.             print ("Yay! Player moved forward!");
    16.         }
    17.     }
    18.  
    19. }
    20.  
    21. /*********************/
    22.  
    23. //We'll say the bad rule moves players backward two tiles.
    24. public class BadRule : Rule {
    25.    
    26.     public override Effect effect {
    27.         get {
    28.             return Rule.Effect.Bad;
    29.         }
    30.     }
    31.    
    32.     void Update() {
    33.         var playersOnTile = GetPlayersOnTile ();
    34.        
    35.         foreach (var player in playersOnTile) {
    36.             player.MoveForwardOrBackward(2);
    37.             print ("Damn. Player moved backward :(");
    38.         }
    39.     }
    40. }
    41.  
    Note, that each of these would be in their own separate script files.
    Next, we drag each of those rule classes to one of the tiles on the board. We'll need to test it, so lets create a quick script that automatically moves the player every, say, 4 seconds:

    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections;
    3.  
    4. //We'll put this script on the player for testing.
    5. [RequireComponent(typeof(Player))]
    6. public class AutoPlay : MonoBehaviour {
    7.  
    8.     //We could be caching this, but it's a quick test so who cares.
    9.     Player player {
    10.         get {
    11.             return GetComponent<Player>();
    12.         }
    13.     }
    14.  
    15.     //We'll use a coroutine for a quick and dirty auto play feature. Basically,
    16.     //We'll just stay in the start method forever, moving the player forward a random
    17.     //number every 4 seconds.
    18.     IEnumerator Start () {
    19.         print ("Game start!");
    20.         //Delay before we move to our first tile.
    21.         yield return new WaitForSeconds(2f);
    22.  
    23.         while (true) {
    24.  
    25.             int random = Random.Range (1,3);
    26.             player.MoveForwardOrBackward(random);
    27.  
    28.             print ("Played moved forward "+random+" times!");
    29.  
    30.             yield return new WaitForSeconds(4f);
    31.         }
    32.    
    33.     }
    34.  
    35. }
    36.  
    Attached working scene here:
     

    Attached Files:

    gamedevindia and LukeDodds like this.
  3. LukeDodds

    LukeDodds

    Joined:
    Oct 24, 2014
    Posts:
    46
    Thank you this is a massive help, have been going over this and your comments understanding parts and finding it really useful!
    Trying to put the code working in my project now and getting a few object reference errors i was hoping you could help me with?

    I tried moving the MoveForwardOrBackwards function into my gamemanger script because i thought there it would be easier to move the player to a select spot with a variable called PlayerOneMovesRolled that is a private variable in the script

    Code (csharp):
    1.  
    2.     public void MoveForwardOrBackward(int times) {
    3.         int targetIndex = PlayerScript.currentTile.Index + times;
    4.         int resolvedIndex = PlayerScript.ResolveIndex (targetIndex);
    5.         PlayerScript.currentTile = Tile.InOrder [resolvedIndex];
    6.     }
    7.  
    8.  



    When these are fixed i was planning on putting this is my move player function
    Code (csharp):
    1.  
    2. MoveForwardOrBackward(PlayerOneMovesRolled);
    3.  
     
  4. BenZed

    BenZed

    Joined:
    May 29, 2014
    Posts:
    524
    Once again, I want to point out that you shouldn't have a separate variable for player one. All of your player should live in a collection of some sort:

    Code (CSharp):
    1. MoveForwardOrBackward(players[0].movesRolled);
    Secondly, the first code snipped doesn't show enough to determine what the null reference is.

    What is PlayerScript? Because that looks like a class name rather than an instance variable name.

    If you're taking that function out of the Player class (which I don't recommend. You can easily call it on the player from your game manager) you'd have to do this:

    Code (CSharp):
    1. public void MoveForwardOrBackward(int times, Player player) {
    2.         int targetIndex = player.currentTile.Index + times;
    3.         int resolvedIndex = player.ResolveIndex (targetIndex);
    4.         player.currentTile = Tile.InOrder [resolvedIndex];
    5.     }
     
  5. LukeDodds

    LukeDodds

    Joined:
    Oct 24, 2014
    Posts:
    46
    Hey the playerscript is the player script included in your example
    Would you mind showing/teaching me the proper way to making a player class then?
    by your reference of players 1 as [0] im assuming its some kind of array or list?
     
  6. gamedevindia

    gamedevindia

    Joined:
    Jun 17, 2015
    Posts:
    4
    @BenZed

    Your fun little Saturday project was literally massive help.
    Also, you have defined each classes very articulately, so that helped me a lot.
    Now I am trying to move the player one tile at a time. That is because in typical board game (like Snakes and Ladders), the player, when moving from one row to another (say from 7 to 13 in Snakes and Ladders), it directly jumps to the final location.

    I've tried following ways so far:
    1. Use WaitForSeconds(t); before passing resolvedIndex to currentTile.
    2. Use System.Threading.Thread.Sleep(t); before passing resolvedIndex to currentTile.
    Both of which didn't work.
    I guessed that was happening because there was transform.position = Vector3.Lerp(transform.position, currentTile.transform.position, Time.deltaTime * 5f); this line in Update function.

    I was wondering if you could please give me some pointers.
     
  7. BenZed

    BenZed

    Joined:
    May 29, 2014
    Posts:
    524
    So, if I understand correctly, if the player is on tile 1, and his next move is to go to tile 4, you'd like the player piece to stop at tiles 2 and 3 on the way. I'd assume this is for aesthetic reasons only, if there are rules or consequences attached to the interim tiles, you don't want the player to experience them, you just want to see him jump from tile to tile.

    You COULD use a Coroutine for this, albeit at a different spot. Multithreading in unity is a little more of a complex topic, and we certainly don't need to use it to accomplish this here.

    I would just change it so that the player tracks the tile it's physically standing on. If it's not on it's physical tile, it Lerps toward it. If it IS on it's physical tile, it updates it until it matches the currentTile.

    Refactored as follows:

    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections;
    3.  
    4. //The player class knows what tile it's on, and contains functions for moving about tiles.
    5. public class Player : MonoBehaviour {
    6.  
    7.     //In the editor, I've just dragged tile 0 into this slot.
    8.     public Tile currentTile;
    9.  
    10.     //The physical tile is the Tile the players gameObject is currently standing on.
    11.     //Only for visual purposes.
    12.     Tile physicalTile;
    13.  
    14.     //If true, when we're jumping from tile to tile, we'll cycle forwards. If false,
    15.     //we'll cycle backwards. This will be handy if we're on tile 0 and are told to go
    16.     //back 1. Without this flag, the player would cycle forward through each tile until
    17.     //it got to the final tile.
    18.     bool moveForwards = true;
    19.  
    20.     void Start() {
    21.         //To start with, the physical tile should be the same tile as the current tile.
    22.         physicalTile = currentTile;
    23.     }
    24.  
    25.     //We want the player to jump to each tile on it's way to it's current tile, if it isn't on the current tile already.
    26.     void Update () {
    27.  
    28.         //Here's where the magic happens. Quite simply, if we're physically on our physicalTile, and that
    29.         //tile isn't the current tile, move the physical tile forward or backward until the physic tile and current tile match.
    30.         if (PhysicallyOnTile (physicalTile) && physicalTile != currentTile) {
    31.             int nextIndex = physicalTile.Index + (moveForwards ? 1 : - 1);
    32.             int resolvedIndex = ResolveIndex(nextIndex);
    33.             physicalTile = Tile.InOrder[resolvedIndex];
    34.         }
    35.                                                               //Notice this is the physicalTile now, not currentTile.
    36.         transform.position = Vector3.Lerp (transform.position, physicalTile.transform.position, Time.deltaTime * 5f);
    37.     }
    38.  
    39.     public bool PhysicallyOnTile (Tile tile) {
    40.      
    41.         return (transform.position - tile.transform.position).magnitude < 0.05f;
    42.      
    43.     }
    44.  
    45.     //Positive for forward, negative for backward.
    46.     public void MoveForwardOrBackward(int times) {
    47.         int targetIndex = currentTile.Index + times;
    48.         int resolvedIndex = ResolveIndex (targetIndex);
    49.         moveForwards = times >= 0;
    50.  
    51.         currentTile = Tile.InOrder [resolvedIndex];
    52.     }
    53.  
    54.     //If we want to move a player to a specific tile (like at the beginning of the game)
    55.     //this function will be handy.
    56.     public void MoveToSpecificTile(int index, bool forwards = true) {
    57.         int resolvedIndex = ResolveIndex (index);
    58.         moveForwards = forwards;
    59.  
    60.         currentTile = Tile.InOrder [resolvedIndex];
    61.     }
    62.  
    63.     //This function could also go in the tile class, but all it does is wrap a given
    64.     //index around in case we try to go to a tile that's above or below the number of tiles we have.
    65.     //In the example scene, if we try to move to index 7, it should put us right back to index 0, completing
    66.     //the circle.
    67.     int ResolveIndex(int input) {
    68.      
    69.         int output = input;
    70.      
    71.         while (output >= Tile.InOrder.Count)
    72.             output -=  Tile.InOrder.Count;
    73.      
    74.         while (output < 0)
    75.             output +=  Tile.InOrder.Count;
    76.      
    77.         return output;
    78.      
    79.     }
    80.  
    81.     //And that is it for our player class!
    82. }
    83.  
    Note that, now we've added the PhysicallyOnTile method to the player. It's public so that Rules and other classes can take advantage of it. In the original example, our Rule.GetPlayersOnTile() method has been refactored as follows:
    Code (CSharp):
    1.     //Handy function we'll want our derived rules to have
    2.     protected Player[] GetPlayersOnTile() {
    3.         var playerList = new List<Player> ();
    4.  
    5.  
    6.         foreach (var player in allPlayers) {
    7.             //If the player's current tile is this tile, and the player is very close to this tile, we'll say it's on this tile. That
    8.             //Way, it wont count if a player is still moving toward this tile from another.
    9.             if (player.currentTile == tile && player.PhysicallyOnTile(tile))
    10.                 playerList.Add (player);
    11.         }
    12.  
    13.         return playerList.ToArray ();
    14.  
    15.     }
    Updated scene attached:
     

    Attached Files:

  8. gamedevindia

    gamedevindia

    Joined:
    Jun 17, 2015
    Posts:
    4
    Thank you very much. I am working on a social project am currently learning Unity3D while developing a 2D board game. :)

    My next step is to assign a tick sound as the player changes position. So is defining sound as a property of tile right approach?

    And yes, moving player from tile to tile was for aesthetics only.
     
  9. BenZed

    BenZed

    Joined:
    May 29, 2014
    Posts:
    524
    Nah, I would assign the tick sound to player.
    Code (CSharp):
    1.         if (PhysicallyOnTile (physicalTile) && physicalTile != currentTile) {
    2.             tickSound.Play();
    3.             int nextIndex = physicalTile.Index + (moveForwards ? 1 : - 1);
    4.             int resolvedIndex = ResolveIndex(nextIndex);
    5.             physicalTile = Tile.InOrder[resolvedIndex];
    6.         }
    Would be the perfect method to play it in.
     
  10. Zeerox_

    Zeerox_

    Joined:
    Jul 30, 2017
    Posts:
    2




    Thank you so much for this code man!
    I was going through your code and i want to move player according to the number come up on my dice so which script to i need to call for that.. i am attaching my dice script . in dice script there is a gamecontrol script which i am calling but thats nothing important just change the dice script according to your script please . please help me out with this i know toooo late . but please do reply (i am working on a snakes and ladder game)
     

    Attached Files:

    • Dice.cs
      File size:
      1.2 KB
      Views:
      718
  11. Zeerox_

    Zeerox_

    Joined:
    Jul 30, 2017
    Posts:
    2

    hey! can you also help me out with the snakes and ladder game? i guess you should have completed the game till now?
     
  12. DocJ

    DocJ

    Joined:
    Dec 9, 2016
    Posts:
    98

    Did you ever get this figured out? I would very much like to know the answer as well. Thanks so much!