0

I'd like to send, receive and cast a subclass of a SkillEvent class and be able to grab custom parameters inside, depending of it's type.

There could be a lot of different subclasses with different parameters.

Is there a way to avoid writing a different method for each kind of skill ?

class SkillEvent { float timestamp, EventType type }
class GrenadeSkillEvent : SkillEvent { Vector3 target_point}

[ClientRpc]
RpcSendSkillEvent(SkillEvent event){
    eventManager.ExecuteEvent(event);
}

ExecuteEvent(SkillEvent event){
    switch(event.type){
        case GrenadeSkillEvent : 
            GrenadeSkillEvent grenadeEvent = (GrenadeSkillEvent) event;
            float grenadeParam = event.grenadeParam;
            break;

        case OtherSkillEvent : 
            OtherSkillEvent otherEvent = (OtherSkillEvent ) event;
            string stringParam = event.stringParam;
            break;
    }
}

I thought it could be done like that, but apparently not.

Is there a way to avoid writing a different method for each kind of skill ?

Edit :

GrenadeSkillEvent and OtherSkillEvent are child of SkillEvent.

They all have a timestamp and a type that I thought I could need to help casting the event variable into the right SkillEvent subclass.

My problem is that they have the same method, let say execute, but every kind of subclass needs a different type of parameter.

For example, a GrenadeEvent could need a target point, the size of the blast and the damages.

A BonusEvent could add some HP to a player.

A TerrainEvent could activate some custom animation on an important object close to the player, etc.

All the logic behind these skill is the same.

They all have 2 methods :

Simulate(SkillEvent Event) and Render(SkillEvent Event)

But in GrenadeSkill for example I need it to be

Simulate(GrenadeSkillEvent Event) and Render(GrenadeSkillEvent Event)

Edit 2 :

networkedPlayer.RpcOnSkillEvent(
    new SkillEvent[] {
        new GrenadeSkillEvent() {
            timestamp = state.timestamp,
            id = 0,
            point = target
        } 
    }
);

Edit 3:

var dictionary = new Dictionary<Type, Action<SkillEvent>> {
    {
        typeof(GrenadeSkillEvent), (SkillEvent e) => {
            Debug.Log("GrenadeSkill OnConfirm point"+ ((GrenadeSkillEvent)e).point);
        }
    }
};

public override void OnConfirm(SkillEvent skillEvent) {

    if (skillEvent == null) return;

    Debug.Log(skillEvent.GetType());
    dictionary[skillEvent.GetType()](skillEvent);
}
FLX
  • 2,626
  • 4
  • 26
  • 57
  • 4
    This is bad practice, though quite a common technique for the less experienced to start trying. Instead, how about create an abstract method on `SkillEvent` and each derived class implements it with the functionality required. – DavidG Apr 16 '18 at 16:32
  • Do `GrenadeSkillEvent ` and `OtherSkillEvent ` derive from `SkillEvent` ? – arekzyla Apr 16 '18 at 16:39
  • Edited with more details – FLX Apr 16 '18 at 16:50
  • Create a `Dictionary` or `Dictionary`, based on you requirement for a given type, fill details and execute a specific method post resolution at runtime – Mrinal Kamboj Apr 16 '18 at 16:52
  • To avoid writing a separate method for each class implement a common method with optional / default parameters and execute them from different classes using relevant parameters – Mrinal Kamboj Apr 16 '18 at 16:54
  • Can you provide the code that invokes the execution method? – Thatalent Apr 16 '18 at 18:05
  • 1
    Yes, see edit 3. Thanks for your help. Each player get 2 skills. Each skill can accept a specific SkillEvent. I thought the hardest part was behind me ;( – FLX Apr 16 '18 at 18:17
  • You're learning an important lesson in type hierarchy design: *don't make polymorphic things which cannot be used in the same way*, because they are not actually polymorphic. – Eric Lippert Apr 16 '18 at 22:52
  • 1
    It also sounds like you are making the common mistake of parallel hierarchy. That is, you have `Animal - Mammal - Giraffe` and then `AnimalCage - MammalCage - GiraffeCage` and `AnimalFeeder - MammalFeeder - GiraffeFeeder`, and so on, where a giraffe feeder feeds a giraffe in a giraffe cage, and so on. Parallel hierarchies are notoriously difficult to get right, since they impose a restriction that cannot be expressed in the type system. – Eric Lippert Apr 16 '18 at 22:56
  • If the subject of type design for games interests you, consider browsing some of these questions and answers: https://stackoverflow.com/search?q=wizards+and+warriors – Eric Lippert Apr 16 '18 at 22:59
  • Thanks, @EricLippert lot of interesting readings. – FLX Apr 17 '18 at 19:22

4 Answers4

2

The short answer is that you could do what you are asking, by making your own Type, but you really don't want to do that. This sort of code is exactly what polymorphism was designed to prevent. To understand why, imagine that you've used this pattern in 5 or 10 different places in your code -- each of them has a switch statement based on Type, and different code that is run in each case. Now let's say you have a new type of SkillEvent to introduce: You have to hunt down every one of the places in your code where you switched on Type and add some more code. All that code that has already been QA'ed you've suddenly opened up, and it all has to be QA'ed again. Plus, what if you forget one of the spots? What a pain, to have to go edit all these disparate places just to add one new concrete instance of your class.

Now consider the right way to do it: for each of these 10 places, you create an abstract method in the base class, and then override it in each of the concrete classes. The abstract method creates a contract, which every concrete class can fulfill in its own way.

In your specific example, you say that you want to extract different parameters from the different concrete classes. What do you plan to do with those parameters? Is it to create a string which describes the object? Is it to pass those parameters to a service call you're about to make? Think in terms of the goal you want to accomplish. I'll assume that it is the latter case, because that is something where you actually need individual parameters. So, make your abstract method defined in the base class be

abstract Map<String, Object> getParameters();

or perhaps

abstract void addParameters(Map<String, Object>);

Now the concrete classes create a map and fill it in with their own parameters, but your calling method doesn't have to know anything about the implementation of this method. That's where you want the knowledge of the GrenadeSkillEvent to be, anyway, inside that class. You don't want some other class to know anything about the details of GrenadeSkillEvent. After all, you might decide to change its implementation in the future, such that it has a new parameter. At that point, you don't want to have to remember to go hunt down the callXYZServiceCall code which has this switch in it. (Worse still, it isn't you adding the new Parameter, but another engineer on the project, and he doesn't even know to worry about this switch statement.)

Zag
  • 638
  • 4
  • 8
1

The best way to do this is by writing a generic getter method on the top class, SkillEvent then overriding the method in classes at are special. Since you seem to need different types I would think that you could either use a method that executes your method that you params inside of the class

Here would be an example with your current code posted:

class SkillEvent { 

  float timestamp; 

  EventType type ;

  public virtual void executeEvent(){ //handles the general case for the execute funciton
    execute(timestamp);
  }
}

class GrenadeSkillEvent : SkillEvent { //overriden to pass target point into event execution

  Vector3 target_point;

  public override void executeEvent(){
    execute(target_point);
  }
}

class BonusEvent : SkillEvent { 

  int bonus_health;

  public override void executeEvent(){/overriden to pass bonus health into event
    execute(bonus_health);
  }
}

class TerrainEvent : SkillEvent { 

  GameObject obj;

  public override void executeEvent(){//overriden to play animation before event is executed
    animation(obj);
    execute();
  }
}

[ClientRpc]
RpcSendSkillEvent(SkillEvent event){
    eventManager.ExecuteEvent(event);
}

ExecuteEvent(SkillEvent event){
    event.executeEvent();
}
Thatalent
  • 404
  • 2
  • 8
  • Declare `ExecuteEvent` as virtual and override it, though here it wouldn't work since OP wants a single method for variety of input – Mrinal Kamboj Apr 16 '18 at 17:19
0

If you use C# 7 and GrenadeSkillEvent and OtherSkillEvent derive from SkillEvent you can use pattern matching:

switch (@event) //@event is of SkillEvent type
{
    case GrenadeSkillEvent gse:
        float grenadeParam = gse.grenadeParam;
        break;
    case OtherSkillEvent ose:
        string stringParam = ose.stringParam;
        break;
    case null:
        //null handling
        break;
    default:
        //other types
        break;
}

If not you can use dictionary for it:

var dictionary = new Dictionary<Type, Action<SkillEvent>>
    {
        {
            typeof(GrenadeSkillEvent), e => 
            {
                float grenadeParam = ((GrenadeSkillEvent)e).grenadeParam;
                //some logic
            }
        },
        {
            typeof(OtherSkillEvent), e => 
            {
                string stringParam = ((OtherSkillEvent)e).stringParam;
                //some logic
            }
        }
    }

if (@event == null)
{
    //null handling
}
else
{
    dictionary[@event.GetType()](@event);
}

Alternatively a more object-oriented approach is to create an abstract method inside SkillEvent and override it in all the concrete classes but you would need to have common return type and parameters.

arekzyla
  • 2,878
  • 11
  • 19
  • Can't use C#7 in Unity, but will try the second, thanks – FLX Apr 16 '18 at 16:54
  • "but you would need to have common return type and parameters." This is the problem, they have the same logic but different types of parameters. I want a single method to receive all the different events from the network and dispatch them – FLX Apr 16 '18 at 17:02
  • "A switch expression of type `System.Type' cannot be converted to an integral type, bool, char, string, enum or nullable type" – FLX Apr 16 '18 at 17:03
  • @FLX look at the dictionary solution – arekzyla Apr 16 '18 at 17:07
  • Only issue with `Dictionary` is that Action doesn't have standard input as per OP for different Skill Event, so this needs some standardization – Mrinal Kamboj Apr 16 '18 at 17:08
  • Each Action can take different set of parameters, thus this needs common structure to execute – Mrinal Kamboj Apr 16 '18 at 17:10
  • Add all the Parameters to a class and Supply that to the Action, here each event can use the required parameters – Mrinal Kamboj Apr 16 '18 at 17:11
  • With dictionnary, I can compile without errors but I get null when trying to get a parameter specific to GrenadeSkillEvent – FLX Apr 16 '18 at 17:17
  • @FLX where do you get this error? Make sure `@event` is not null before. – arekzyla Apr 16 '18 at 17:30
  • GetType() is returning SkillEvent – FLX Apr 16 '18 at 17:36
  • @FLX if it's returning SkillEvent it's of type SkillEvent. GetType always returns the actual type even if it's referenced by base type. So for `SkillEvent a = new OtherSkillEvent();` the expression `a.GetType()` returns `OtherSkillEvent` – arekzyla Apr 16 '18 at 17:40
  • See edit 2 for my instanciation code. The object is created as GrenadeSkillEvent, but I send it inside an array of SkillEvent. Maybe this is the problem – FLX Apr 16 '18 at 17:46
0

Idk, it just idea. (not idea, i using same methods)

Maybe good solution will be is using data base of skills, and sending RPC with string params instead serializable classes (your SkillEvent)

So, using this data base of skills, you can execute code with string params as you wish.

You can put skill id and params to string, send rpc, receive command, parse strings, execite code by id skill with params.

  1. Put skill params and skill id to string
  2. Send RPC with this strings
  3. Receive RPC
  4. Parse string into your format (in this exlample into jsonObject)
  5. Find used skill by id_skill and execute code with params
  6. profit!

Helpful tool to parse, or create strings, will be some JSON parser (serializator.. idk)

Maybe you ask me: "why strings and json?"

I answer you: "because it's flexible!"

Examples:

void UseGrenade(){ // when some player using grenade skill.
    JsonObject json = new JsonObject();
    json.addField("id_skill", id); // id of grenade skill
    json.addField("timestamp", 1.5f);
    json.addField("position", targetPosition);
    SendRpcSkill(json.Print()); // converting json to string and sending this string
}

For example params to execute grenade:

 // it is pseudo json for example
{
    id_skill: 1, // id of grenade
    timestamp: 1.5,
    position: {1,21,3}
}

Or some other skills:

// it is pseudo json for example
{
    id_skill: 2, // id of some skill
    timestamp: 1.8,     
    msg:"rain of fishes for cats!",
    // ... and other params
}

And to Execute this skills:

[ClientRpc]
RpcSendSkill(string skillData){
    JsonObject json = new JsonObject(skillData); //converting string to json
    int idskill = json["id_skill"]; // get id of used skill
    mySkillsDataBase[idskill].Execute(json); // find skill and execute code
}

Somewhere in your game:

public class GrenadeSkill: BaseSkill{

    public override void Execute(JsonObject json){
       float timeStamp = (int)json["timestamp"];        
       Vector3 targetPosition = (Vector3)json["position"];
       // ...
       // and you grenade logic
    }
}

I hope you understand my idea.

nipercop
  • 357
  • 1
  • 11
  • Thanks for your answer. Look like dynamic casting is not possible because of the way unity is serializing network data. I wanted to avoid json because things i read about overhead and garbage collection, but will probably use a similar solution using byte arrays / custom NetworkWriter – FLX Apr 24 '18 at 09:15