Search Unity

How would you program that [Global Effect] of card game?

Discussion in 'Scripting' started by leegod, May 11, 2016.

  1. leegod

    leegod

    Joined:
    May 5, 2010
    Posts:
    2,476
    Hi.
    For example, at Magic the gathering card game, there are artifact and enchantment. It is sort of Global effect skills. And if some conditions are met, their effect being executed.

    How program this?

    Using event keyword and publisher & subscriber pattern?
     
  2. JoshuaMcKenzie

    JoshuaMcKenzie

    Joined:
    Jun 20, 2015
    Posts:
    916
    I would have a singleton manager that has a dictionary of Trigger events (each event passes in the gameobject that triggered the event and return void) and an extra event that all cards and players subscribe to, the dictionary will store all the trigger types that could trigger a global ability(for example "OnCardCast" and "OnPlayerTurn") . trigger events are specifically about the when, not the how.

    CardAbilities (such as an enchament that "heals all players when any card is cast", or an artifact that, "at the beginning of any turns, deal damage to all players and monsters, then heal the owner by 1 for each killed this way") will listen to the relevent trigger events in the dictionary so they know when to activate their global effect. when a card with a CardAbilities is played (or if it can be triggered in hand) it will subscribe itself to the trigger dictionary and run its internal TriggerHandler script (like WhenSpellIsCast() and WhenOpponentsTurnBegins()) will when the trigger is called.

    The second standalone event will pass in both an EffectCallback ( which is defined on the card with the cardTrigger ) and a "out EventStateObject" that all the listening cards in can populate and share with so that enchantment card can get return data all allocated in an singe object (since you can't really use return on an event handler too effectively) the EffectCallback also expects a gameObject parameter. So each card will invoke the EffectCallback passing their gameObject and the EventStateObject as parameters. The EffectCallback will validate if the global effect applies to the gameObject, (like if gameObject.getComponent<Player>() != null) and if so applies the effect on them (player.Life++;) otherwise its returns and does nothing. The EffectCallback works on the Who and the how, but by keeping it all in the card's class

    the EventStateObject is just a simple empty class, but an artifact card can define a nested, class that inherits from EventStateObject like:
    Code (CSharp):
    1. public class FeedOnTheWeak : EventStateObject
    2. {
    3.    public int monsterskilled;
    4. }
    and using polymorphism it still adheres to the global even definition set forth.
    then the initial handler that was listening to the trigger dictionary can use that Feed on the Weak effect to then heal the player equal to the number of monsters that died.

    this setup is mostly decoupled, every card must know about the Event manager, and CardTrigger Cards must know other component types (like Player, Card, Goblin, Red, Monster, etc...) thats relevent to their effects. However the setup is kept mostly abstract so that its easy to introduce new types of cards.

    Most of the time such a complex system is not needed. But if this is a type of card that will commonly get new cards, than its important that you use decoupled systems to maintain developmental agility.
     
    Kiwasi likes this.
  3. leegod

    leegod

    Joined:
    May 5, 2010
    Posts:
    2,476
    @joshua
    Thanks for answer, its very good and promising, I want to implement, but a bit hard. And can't understand 100% until I see the real code. Can you share the core mechanical code?
    What the code of [Trigger events (each event passes in the gameobject that triggered the event and return void)] should be?
     
    Last edited: May 11, 2016
  4. leegod

    leegod

    Joined:
    May 5, 2010
    Posts:
    2,476
    I can't understand your second part, 'Second Standalone event' ? How implement?

    and I don't know well how use dictionary like your said.

    So I wrote like this,

    Code (CSharp):
    1. using System;
    2. using UnityEngine;
    3. using System.Collections;
    4. using System.Collections.Generic;
    5.  
    6. [System.Serializable]
    7. public abstract class Card
    8. {
    9.     public string name;
    10. }
    11.  
    12. [System.Serializable]
    13. public class Player
    14. {
    15.     public string name;
    16.     public int hp;
    17. }
    18.  
    19. public interface ITrigger
    20. {
    21.     bool ConditionMet();
    22. }
    23.  
    24. public class OnCardCast : ITrigger
    25. {
    26.     public event EventHandler<CardEventArgs> onCard;
    27.  
    28.     public void OnCast(CardEventArgs e)
    29.     {
    30.         EventHandler< CardEventArgs> handler = onCard;
    31.         if (handler != null && ConditionMet())
    32.             handler(this, e);
    33.     }
    34.  
    35.     public bool ConditionMet()
    36.     {
    37.         if (true) // if Card Casted,
    38.         {
    39.             Debug.Log("Card casted and condition are met!");
    40.             return true;
    41.         }
    42.     }
    43.    
    44. }
    45. public class OnPlayerTurn : ITrigger
    46. {
    47.     public bool ConditionMet ()
    48.     {
    49.         if (true) // if player turn,
    50.         {
    51.             Debug.Log("Player turn and condition are met!");
    52.             return true;
    53.         }
    54.     }
    55. }
    56.  
    57. public class GlobalCard : Card
    58. {
    59.     public GlobalCard(OnCardCast occ)
    60.     {
    61.         occ.onCard += Execute;
    62.     }
    63.     void Execute(object sender, CardEventArgs e)
    64.     {
    65.         Debug.Log(e.val+"This is global effect");
    66.     }
    67. }
    68. public class GlobalEffectTest : MonoBehaviour
    69. {
    70.     public static GlobalEffectTest Instance;
    71.     public Dictionary<ITrigger, string> TriggerDictionary = new Dictionary<ITrigger, string>();
    72.  
    73.     void Awake()
    74.     {
    75.         Instance = this;
    76.     }
    77.     void Start () {
    78.         OnCardCast occ = new OnCardCast();
    79.         GlobalCard gc = new GlobalCard(occ);
    80.         CardEventArgs cardEvent = new CardEventArgs(3);
    81.         occ.OnCast(cardEvent);
    82.     }
    83.  
    84.     void Update () {
    85.  
    86.     }
    87. }
    88.  
    89. public class CardEventArgs : EventArgs
    90. {
    91.     public int val;
    92.     public CardEventArgs(int num)
    93.     {
    94.         val = num;
    95.     }
    96. }
    97.  
     
  5. JoshuaMcKenzie

    JoshuaMcKenzie

    Joined:
    Jun 20, 2015
    Posts:
    916
    Sorry for the delay. I've been too busy to give a proper answer to properly clarify.

    I would first recommend watching the event messaging system live traning video. a large portion of my code is almost i direct copy. I like it since its easy to add new events in the future without even having to touch the event manager. also it can give you a better understanding of the dictionary.

    below if just a rough draft of the event system I'm talking about. I actually spent more time trying to think of better names than job, task and action than I did typing it out.

    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEngine.Events;
    3. using System.Collections;
    4. using System.Collections.Generic;
    5.  
    6. public abstract class EventStateData{}
    7. public delegate void Job<GameObject, EventStateData>(GameObject go,out EventStateData eventstateData);
    8.  
    9. public delegate void GlobalEffect<EventStateData, GlobalEffectCallback>(out EventStateData eventstateData,Job<GameObject, EventStateData> job);
    10. public class EventManager : MonoBehaviour
    11. {
    12.     public class GlobalEffect: UnityEvent<EventStateData,Job<GameObject, EventStateData>>{}
    13.     public class Trigger: UnityEvent<GameObject>{}
    14.  
    15.  
    16.     private Dictionary <string, Trigger> triggers;
    17.     private GlobalEffect globalEffect;
    18.  
    19.     private static EventManager eventManager;
    20.  
    21.     public static EventManager instance
    22.     {
    23.         get
    24.         {
    25.             if (!eventManager)
    26.             {
    27.                 eventManager = FindObjectOfType (typeof (EventManager)) as EventManager;
    28.  
    29.                 if (!eventManager)
    30.                 {
    31.                     Debug.LogError ("There needs to be one active EventManger script on a GameObject in your scene.");
    32.                 }
    33.                 else
    34.                 {
    35.                     eventManager.Init ();
    36.                 }
    37.             }
    38.  
    39.             return eventManager;
    40.         }
    41.     }
    42.  
    43.     void Init ()
    44.     {
    45.         if (triggers == null)
    46.         {
    47.             triggers = new Dictionary<string, UnityEvent>();
    48.         }
    49.     }
    50.  
    51.     void OnDestroy()
    52.     {
    53.         for (int i = triggers.Count - 1; i >= 0; i--)
    54.         {
    55.             triggers [i].RemoveAllListeners ();
    56.         }
    57.         triggers.Clear ();
    58.  
    59.         globalEffect = null;
    60.  
    61.     }
    62.  
    63.     public static void AddToEffects(GlobalEffect<EventStateData, Job<GameObject, EventStateData>> action)
    64.     {
    65.         instance.globalEffect += action;
    66.     }
    67.  
    68.     public static void RemoveFromEffects(GlobalEffect<EventStateData, Job<GameObject, EventStateData>> action)
    69.     {
    70.         instance.globalEffect -= action;
    71.     }
    72.  
    73.     public static void InvokeGlobalEffect(out EventStateData stateData, Job<GameObject, EventStateData> job)
    74.     {
    75.         if (instance.globalEffect != null)
    76.         {
    77.             instance.globalEffect(out stateData, callback);  
    78.         }
    79.     }
    80.  
    81.  
    82.     public static void StartListening (string eventName, UnityAction listener)
    83.     {
    84.         Trigger thisEvent = null;
    85.         if (instance.triggers.TryGetValue (eventName, out thisEvent))
    86.         {
    87.             thisEvent.AddListener (listener);
    88.         }
    89.         else
    90.         {
    91.             thisEvent = new UnityEvent ();
    92.             thisEvent.AddListener (listener);
    93.             instance.triggers.Add (eventName, thisEvent);
    94.         }
    95.     }
    96.  
    97.     public static void StopListening (string eventName, UnityAction listener)
    98.     {
    99.         if (eventManager == null) return;
    100.         Trigger thisEvent = null;
    101.         if (instance.triggers.TryGetValue (eventName, out thisEvent))
    102.         {
    103.             thisEvent.RemoveListener (listener);
    104.         }
    105.     }
    106.  
    107.     public static void Trigger (string eventName,GameObject source)
    108.     {
    109.         Trigger thisEvent = null;
    110.         if (instance.triggers.TryGetValue (eventName, out thisEvent))
    111.         {
    112.             thisEvent.Invoke (source);
    113.         }
    114.     }
    115. }
    116.  
    117. public sealed class Card: MonoBehaviour
    118. {
    119.     public CardProperties properties { get; set; }
    120.  
    121.     void OnEnable()
    122.     {
    123.         properties = GetComponent<CardProperties>();
    124.         EventManager.AddToEffects (Participate);
    125.  
    126.         EventManager.Trigger ("OnCardEnabled");
    127.     }
    128.     void OnDisable()
    129.     {
    130.         EventManager.RemoveFromEffects (Participate);
    131.     }
    132.  
    133.     public void Cast()
    134.     {
    135.             //validate that you can cast card, ten cast it
    136.  
    137.             EventManager.Trigger ("OnCardCast", gameObject);
    138.     }
    139.  
    140.     void Participate(out EventStateData eventstateData,GlobalEffectCallback<GameObject, EventStateData> job)
    141.     {
    142.         job(gameObject,out eventstateData);
    143.     }
    144. }
    145.  
    146. // when a black card is cast, Card Owner gains life equal to black cards in play
    147. public sealed class BlackChaliceEnchanment:MonoBehaviour
    148. {
    149.     public CardProperties properties { get; set; }
    150.     public class EnchantmentData:EventStateData
    151.     {
    152.         public int BlackCardsInPlay;
    153.     }
    154.  
    155.     void OnEnable()
    156.     {
    157.         properties = GetComponent<CardProperties>();
    158.         EventManager.StartListening ("OnCardCast", WhenACardIsCast);
    159.     }
    160.  
    161.     void OnDisable()
    162.     {
    163.         EventManager.StopListening ("OnCardCast", WhenACardIsCast);
    164.     }
    165.  
    166.     void WhenACardIsCast(GameObject source)
    167.     {
    168.         if (source.GetComponent<BlackCard> () == null)
    169.         {
    170.             return; //source is not a black card enchantment doesn't trigger
    171.         }
    172.  
    173.         EnchantmentData data = new EnchantmentData ();
    174.         data.BlackCardsInPlay = 0;
    175.  
    176.         EventManager.InvokeGlobalEffect(out data,GlobalEffect);
    177.         properties.currentOwner.Life += data.BlackCardsInPlay;
    178.  
    179.     }
    180.  
    181.     void GlobalEffect(GameObject gameObject,out EventStateData eventstateData)
    182.     {
    183.         BlackCard blackCard = gameObject.GetComponent<BlackCard> ();
    184.  
    185.         if (blackCard == null)
    186.             return;
    187.  
    188.         if (blackCard.properties.IsInPlay)
    189.         {
    190.             EnchantmentData data = eventstateData as EnchantmentData;
    191.             data.BlackCardsInPlay++;
    192.         }
    193.     }
    194. }
    Similar to my code, and since I know how Trading cards can get... I would try to make your classes SUPER modular so if a card is a black card, then the game object should have a BlackCard component script and a Card component script. the Enchantment card in my example would have a Card Component, a BlackChaliceEnchantment Component, a Enchantment Component, and maybe a BlackCard Component as well . Looking for a card that is either Red or Black? well you may invent a card that is both Red and Black (the card will have both components) and so it should pop up for the current set of cards, last season's cards, and possibly any future cards.

    These classes may also benefit by not holding any state (immutible), or as best as possible, and instead reference a single script that will store the state (CardProperties) which all the components can share with which will also reduce inconsistent data (bug example: one component may say the card is tapped, another says its not). it also abstracts the data from the components which will make adding a feature or fixing a bug only require you to change 1-2 files instead of 10-20, even 100-200 depending on how many card types you can think up.
     
  6. leegod

    leegod

    Joined:
    May 5, 2010
    Posts:
    2,476
    @joshua
    Very thanks for answer, I just saw and copied above script to newly made unity empty project, made script name as 'EventManager' and copied your first part script, but occurs many errors. Anyway I would look into more.
     
  7. leegod

    leegod

    Joined:
    May 5, 2010
    Posts:
    2,476
    So I revised error part and made it worked.
    Though I dont know well why it needed to use out keyword and eventstatedata, and job syntax. Just I want to understand thoroughly.
     
  8. JoshuaMcKenzie

    JoshuaMcKenzie

    Joined:
    Jun 20, 2015
    Posts:
    916
    Since you're raising an event you can't simply use a return to get data from every object that participated. event handlers typically return void. if it didn't you would get bum rushed by possibly hundreds of different objects each trying to force their results down your throat and into a single value. So instead you craft a Memento object (from the memento design pattern) and give that to all the listeners. the memento is a single object which all the listeners will share and accumulate their data into. and when the event is done you'll already have a reference to that same memento instance that you gave to everyone.

    Think of it like a donation basket at mass. You hand it off and let it get passed around and when its done you get all the stuff you want consolidated into a single container. The beauty of the memento pattern is how you're able to iterate over cards that have no idea how the enchantment works. from their point of view they just run a function inserting themselves into the handler function and let the handler worry about what it needs.

    This decouples your cards form other cards. So if you make new cards, cardA only worries about its own special rules and uses Participate () so that cardB can enact its own rules on the cardA without cardA needing to know or even care what those new rules are. This makes adding new rules pretty easy as now you don't need to touch the code in the Card class which might break another enchantment (which brings up the importance that you start up unit testing early so that a broken card doesn't sneak into the build without your notice)

    The job syntax is just defining a special type of function reference, a datatype. At the top I claim that there is a delegate, which I call "Job" and any function that the Job delegate will point to must have these parameters X (the GameObject and the EventStateData) and must return this type Y (void). so you can describe an abstract function to call which you can swap out concrete functions in real-time in one turn you may have an artifact that wants everyone to run it's global effect, and in another turn an enchanment card wants to apply its effect. if you look at the Black Chalice example i'm bascially saying that if an "OnCardCast" event triggers and the source is a black card. than I give the eventManager my GlobalEffect function (notice that it has the same parameters and return type as the Job delegate) and I tell the EventManager to command everyone listening to participate in running the passed function.

    Edit: I realized I answered why I used the "out" word, not what it is. Normally a function only passes data back through a return. however sometimes that is not enough. Physics.RayCast is a good example of this. it returns a true/false determining wheter it hit anything or not, but the raycast also needs to somehow pass the RayCastHit info back to the caller so that they know what to do with it. the out keyword means that I'm giving the function a reference to an object that I want them to affect, and when the function is done that object will have its data worked on and in the variable the I passed into the out parameter.

    there is another reserved that's very similar to the "out" word,"ref". the only difference between out and ref is that the out version must be instantiated before its call, while ref must not.
     
    Last edited: May 15, 2016