synopsis from article index: Expanding on the previous article, this is a discussion of the data objects, camera manager, and game object representing the cards in the Memory game from last week's article.
Unity Essentials Weekly
Memory Game (Part 2 of 3)
Article Table of Contents
Memory Part 2
In this article, I am going to explain the data objects used in the memory game, as well as the game object, and camera manager. The game itself will be discussed in Memory Part 3, next week. This article explains the reasoning behind the specific features for this game, and some other options tried before settling on the final versions.
PlayingCard Data Object
When starting off the Memory game project, I first developed the data objects (sometimes called entities or data model objects) that I will need for representing the cards in the game. Unity does not require all c# to subclass from MonoBehaviour. Just put whatever c# files you would like anywhere in the project. One limitation is you can't use namespaces on these files. You may seem to get away with it at first, but the Unity manual says not to, and I've always eventually ran into compiling problems down the road when I ignored that advice.
The PlayingCard object was an obvious requirement for the project. It represents the value of one card from the game, containing the rank and suit as enumerated types. I overrode the comparison method, Equals, so two cards can be compared using card1 == card2. This caused one point that should be discussed: you have to override the GetHashCode method as well when overriding the equals operator.
1: /// <summary>
2: /// Overrides = to compare the values of the two cards.
3: /// </summary>
4: public override bool Equals(object o)
5: {
6: if (o != null && o is PlayingCard)
7: {
8: PlayingCard other = o as PlayingCard;
9: return rank == other.rank
10: && suit == other.suit;
11: }
12: return false;
13: }
14:
15: /// <summary>
16: /// Required for overriding equals.
17: /// </summary>
18: public override int GetHashCode()
19: {
20: return rank.GetHashCode() + suit.GetHashCode();
21: }
GetHashCode is used by list objects in .Net to check to see if two object references contain the same value. I satisfied this requirement by adding the hash codes returned by the Suit and Rank values. To be honest, this should probably multiply one of the values by the length of the other to ensure unique values, but I was just looking for the compiler to shut up. A detailed overview for GetHashCode can be found here if you are curious about the feature.
I could have also overridden the less than and greater than operators for this object, but our requirements for this game don't need them, so I left them out. There is also a simple ToString implementation, so the value becomes something like "QH" when assigned to a string. This is used for naming the game objects for the cards as they are generated, so I can see what is going on in the scene's Hierarchy at a glance.
PlayingCardDeck Data Object
The other data object I created was the PlayingCardDesk object, which I didn't use at first. Originally I was returning random cards generated by the PlayingCard object, but this caused repeated values occasionally. This deck object generates an entire deck of cards and shuffles the deck by randomly swapping the position of two cards 1000 times, and the card is removed from the deck each time one is selected to ensure unique values.
1: using UnityEngine;
2: using System.Collections.Generic;
3:
4: /// <summary>
5: /// Represents a shuffled deck of cards to choose from.
6: /// </summary>
7: public class PlayingCardDeck
8: {
9: #region Constructors
10:
11: /// <summary>
12: /// Creates a shuffled deck.
13: /// </summary>
14: public PlayingCardDeck()
15: {
16: this.FillDeck();
17: this.Shuffle();
18: }
19:
20: #endregion
21:
22: #region Properties
23:
24: /// <summary>
25: /// Current deck of cards.
26: /// </summary>
27: public List<PlayingCard> cards;
28:
29: #endregion
30:
31: #region Methods
32:
33: /// <summary>
34: /// Returns and removes a random card from the deck.
35: /// </summary>
36: public PlayingCard GetRandomCard()
37: {
38: int count = this.cards.Count;
39: int i1 = Random.Range(0, count - 1);
40: PlayingCard ret = this.cards[i1];
41: this.cards.Remove(ret);
42: return ret;
43: }
44:
45: #endregion
46:
47: #region Private
48:
49: /// <summary>
50: /// Initializes the deck with 52 cards.
51: /// </summary>
52: private void FillDeck()
53: {
54: this.cards = new List<PlayingCard>();
55: for (int suit = 1; suit <= 4; suit++)
56: {
57: for (int rank = 1; rank <= 13; rank++)
58: {
59: PlayingCard card = new PlayingCard();
60: card.suit = (PlayingCard.Suit)suit;
61: card.rank = (PlayingCard.Rank)rank;
62: this.cards.Add(card);
63: }
64: }
65: }
66:
67: /// <summary>
68: /// Swaps random pairs 1000 times.
69: /// </summary>
70: private void Shuffle()
71: {
72: int count = this.cards.Count;
73: for (int i = 0; i < 1000; i++)
74: {
75: int i1 = Random.Range(0, count - 1);
76: int i2 = Random.Range(0, count - 1);
77: if (i1 != i2)
78: {
79: PlayingCard temp = this.cards[i1];
80: this.cards[i1] = this.cards[i2];
81: this.cards[i2] = temp;
82: }
83: }
84: }
85:
86: #endregion
87: }
PlayingCardObject - GameObject Representing PlayerCards
The PlayingCardObject is a game object that represents one card from the deck, with methods for instantiating the object and its two children, the front and back of the card. The object contains a PlayingCard value, a static variable storing the size to use when creating cards, and a listener "event".
I'm using quotes for "event" because its not really a c# event, but serves our purpose. This is a reference to an object using the ICardClickListener interface, allowed the card to communicate to the main game that the card was clicked on. This click is detected through the Unity event OnMouseUp, which is called on Colliders when the mouse is released over the card. I will have more details on the interface itself in the next article.
1: using UnityEngine;
2: using System.Collections;
3:
4: /// <summary>
5: /// Definitation for objects that want to know what cards are clicked.
6: /// </summary>
7: public interface ICardClickListener
8: {
9: void OnCardClicked(PlayingCardObject c);
10: }
11:
12: /// <summary>
13: /// Game object for a playing card on the screen.
14: /// </summary>
15: [RequireComponent(typeof(Collider))]
16: public class PlayingCardObject : MonoBehaviour
17: {
18: #region Properties
19:
20: /// <summary>
21: /// Size of the cards
22: /// </summary>
23: public static Vector3 cardSize = new Vector3(0.63f, 0.88f, 0.025f);
24:
25: /// <summary>
26: /// Card shown by this game object.
27: /// </summary>
28: public PlayingCard card;
29:
30: /// <summary>
31: /// Game object for the front of this card
32: /// </summary>
33: public GameObject front;
34:
35: /// <summary>
36: /// Game object for the back of this card
37: /// </summary>
38: public GameObject back;
39:
40: /// <summary>
41: /// Object receiving click events.
42: /// </summary>
43: public ICardClickListener listener;
44:
45: #endregion
46:
47: #region Methods
48:
49: /// <summary>
50: /// Assigns material for this object, and calculates the texture offset
51: /// assuming the texture shows all of the cards.
52: /// </summary>
53: public void AssignTexture(Material m, Material back)
54: {
55: float w = 1f / 13f;
56: float h = 1f / 4f;
57: Vector2 tiling = new Vector2(w, h);
58: Vector2 offset = new Vector2();
59: offset.x = ((int)card.rank - 1) * w;
60: offset.y = ((int)card.suit - 1) * h;
61: this.front.renderer.material = m;
62: this.front.renderer.material.SetTextureOffset("_MainTex", offset);
63: this.front.renderer.material.mainTextureScale = tiling;
64: this.back.renderer.material = back;
65: }
66:
67: /// <summary>
68: /// Creates a new cube representing a certain card.
69: /// </summary>
70: public static PlayingCardObject Create( Object prefab,
71: PlayingCard value,
72: Vector3 position)
73: {
74: Object o = Instantiate(prefab);
75:
76: GameObject go = o as GameObject;
77: go.name = value.ToString();
78: go.transform.position = position;
79:
80: PlayingCardObject card = go.GetComponent<PlayingCardObject>();
81: card.card = value;
82: return card;
83: }
84:
85: #endregion
86:
87: #region Unity Events
88:
89: /// <summary>
90: /// Detects mouse clicks on the card and passes them to the event
91: /// listener.
92: /// </summary>
93: void OnMouseUp()
94: {
95: if (listener != null)
96: {
97: listener.OnCardClicked(this);
98: }
99: }
100:
101: #endregion
102: }
The Create and AssignTexture methods assigns the appropriate front and back to the card using a trick providing one image for the back and another image for all possible fronts of the card. The texture from the front uses Unity's tiling functionality to only display the desired card. This trick is most commonly applied to websites using CSS. You can learn more about the trick in that context here.
CameraManager
I wanted to provide a very simple interface for selecting the cameras in the game. The game provides two views, the normal and cheater view, where the cheater view allows you to play while looking at the front of the cards. All of the available cameras are displayed in the upper-left corner, with buttons allowing for different cameras to be selected.
The camera manager works by looking at all of its transform's children. The children of a transform are accessed by using a foreach statement on the transform object, which iterates through the children. Each child is added to a list, and the default camera assigned by the defaultCamera property is used by default, when available.
While I could have used references to cameras or one of the search methods to look for all components of type Camera, I used this method because it provides a simple and intuitive interface when editing the scene: all of the cameras to use are under the "Cameras" transform in the scene's hierarchy.
1: using UnityEngine;
2: using System.Collections.Generic;
3:
4: /// <summary>
5: /// Keeps a single camera enabled from all of the child objects of
6: /// this tranform.
7: /// </summary>
8: public class CameraManager : MonoBehaviour
9: {
10: #region Properties
11:
12: /// <summary>
13: /// The camera to be displayed at the start.
14: /// </summary>
15: public Camera defaultCamera;
16:
17: #endregion
18:
19: #region Unity Events
20:
21: /// <summary>
22: /// Gathers up the child cameras and enables the default.
23: /// </summary>
24: void Start()
25: {
26: this.GatherCameras();
27: this.SwitchTo(defaultCamera);
28: }
29:
30: /// <summary>
31: /// Displays a label for the selected camera and a button for all
32: /// other cameras.
33: /// </summary>
34: void OnGUI()
35: {
36: foreach (GameObject g in this.cameras)
37: {
38: if (g == this.currentCamera)
39: {
40: GUILayout.Label(g.name);
41: }
42: else
43: {
44: if (GUILayout.Button(g.name))
45: {
46: this.SwitchTo(g);
47: }
48: }
49: }
50: }
51:
52: #endregion
53:
54: #region Private Methods
55:
56: /// <summary>
57: /// Switches which camera is enabled.
58: /// </summary>
59: private void SwitchTo(GameObject g)
60: {
61: if (g != null && g != this.currentCamera)
62: {
63: // disable old selection
64: if (this.currentCamera != null)
65: {
66: this.currentCamera.active = false;
67: }
68:
69: // enable new selection
70: g.camera.enabled = true;
71: AudioListener al = g.GetComponent<AudioListener>();
72: if (al != null)
73: {
74: al.enabled = true;
75: }
76: g.active = true;
77:
78: // save selection
79: this.currentCamera = g;
80: }
81: }
82:
83: /// <summary>
84: /// Overload for switching using the camera object.
85: /// </summary>
86: private void SwitchTo(Camera c)
87: {
88: if (c != null)
89: {
90: this.SwitchTo(c.gameObject);
91: }
92: }
93:
94: /// <summary>
95: /// Searches for cameras, adding them to a list and disables each
96: /// of them.
97: /// </summary>
98: private void GatherCameras()
99: {
100: this.cameras = new List<GameObject>();
101: foreach (Transform child in this.transform)
102: {
103: if (child.gameObject.camera != null)
104: {
105: this.cameras.Add(child.gameObject);
106: child.gameObject.active = false;
107: }
108: }
109: }
110:
111: #endregion
112:
113: #region Private Properties
114:
115: /// <summary>
116: /// List of cameras to switch between.
117: /// </summary>
118: private List<GameObject> cameras;
119:
120: /// <summary>
121: /// The object containing the camera currently in use.
122: /// </summary>
123: private GameObject currentCamera;
124:
125: #endregion
126: }
The Unity Essentials Weekly blog is brought to you by GSD Software under our Unity Essentials brand name.
This product is presented to you free of charge, in the hope that spreading knowledge about Unity will lead to more development using Unity, which will lead to more projects for our company involving Unity, because we think it's the best thing since slice bread as far as 3D platforms go.
GSD Software is a Tempe, Arizona based company, specializing in web development, desktop software development, and virtual worlds.