Приведение различных подклассов в методе

Я хотел бы отправлять, получать и разыгрывать подкласс класса SkillEvent и иметь возможность получать пользовательские параметры внутри, в зависимости от его типа.

Там может быть много разных подклассов с разными параметрами.

Есть ли способ избежать написания разных методов для каждого вида навыков?

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;
    }
}

Я думал, что это можно сделать так, но, видимо, нет.

Есть ли способ избежать написания разных методов для каждого вида навыков?

Редактировать:

GrenadeSkillEvent и OtherSkillEvent являются дочерними элементами SkillEvent.

У них у всех есть метка времени и тип, который, как мне показалось, может понадобиться для приведения переменной события в правильный подкласс SkillEvent.

Моя проблема в том, что у них один и тот же метод, скажем, execute, но для каждого вида подкласса нужен свой тип параметра.

Например, GrenadeEvent может потребоваться целевая точка, размер взрыва и повреждения.

BonusEvent может добавить немного здоровья игроку.

TerrainEvent может активировать некоторую пользовательскую анимацию на важном объекте рядом с игроком и т. Д.

Вся логика этих навыков одинакова.

Все они имеют 2 метода:

Simulate(SkillEvent Event) and Render(SkillEvent Event)

Но в GrenadeSkill например мне нужно чтобы это было

Simulate(GrenadeSkillEvent Event) and Render(GrenadeSkillEvent Event)

Изменить 2:

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

Изменить 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);
}

4 ответа

Короткий ответ: вы можете делать то, что просите, создав собственный тип, но вы действительно не хотите этого делать. Этот тип кода является именно тем, что полиморфизм был разработан, чтобы предотвратить. Чтобы понять почему, представьте, что вы использовали этот шаблон в 5 или 10 разных местах в вашем коде - у каждого из них есть оператор switch, основанный на Type, и разный код, который запускается в каждом случае. Теперь предположим, что у вас есть новый тип SkillEvent, который нужно представить: вы должны найти все места в вашем коде, где вы включили тип, и добавить еще немного кода. Весь тот код, который уже был проверен, вы внезапно открыли, и все это должно быть снова проверено. Плюс, что если ты забудешь одно из мест? Какая боль, когда приходится редактировать все эти разные места, чтобы добавить один новый конкретный экземпляр вашего класса.

Теперь рассмотрим правильный способ сделать это: для каждого из этих 10 мест вы создаете абстрактный метод в базовом классе, а затем переопределяете его в каждом из конкретных классов. Абстрактный метод создает контракт, который каждый конкретный класс может выполнить по-своему.

В вашем конкретном примере вы говорите, что хотите извлечь разные параметры из разных конкретных классов. Что вы планируете делать с этими параметрами? Это создать строку, которая описывает объект? Это передать эти параметры вызову службы, который вы собираетесь сделать? Думайте с точки зрения цели, которую вы хотите достичь. Я предполагаю, что это последний случай, потому что это то, где вам действительно нужны индивидуальные параметры. Итак, сделайте ваш абстрактный метод, определенный в базовом классе, следующим

abstract Map<String, Object> getParameters();

или возможно

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

Теперь конкретные классы создают карту и заполняют ее собственными параметрами, но ваш вызывающий метод не должен ничего знать о реализации этого метода. Вот где вы хотите знания GrenadeSkillEvent в любом случае, быть внутри этого класса. Вы не хотите, чтобы какой-то другой класс знал что-либо о деталях GrenadeSkillEvent, В конце концов, вы можете решить изменить его реализацию в будущем, чтобы у него был новый параметр. В этот момент вам не нужно помнить, чтобы вы охотились на callXYZServiceCall код, который имеет этот переключатель в нем. (Хуже того, это не вы добавляете новый параметр, а другого инженера в проект, и он даже не знает, что беспокоиться об этом утверждении переключателя.)

Лучший способ сделать это - написать общий метод получения в верхнем классе, SkillEvent затем переопределение метода в классах в являются специальными. Поскольку вам, похоже, нужны разные типы, я думаю, что вы можете использовать метод, который выполняет ваш метод, который вы указали в классе

Вот пример с вашим текущим кодом:

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();
}

Если вы используете C# 7 и GrenadeSkillEvent а также OtherSkillEvent вытекают из SkillEvent Вы можете использовать сопоставление с образцом:

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;
}

Если нет, вы можете использовать словарь для этого:

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);
}

В качестве альтернативы более объектно-ориентированный подход заключается в создании абстрактного метода внутри SkillEvent и переопределить его во всех конкретных классах, но вам нужно иметь общий тип и параметры возврата.

Идк, это просто идея. (не идея, я использую те же методы)

Возможно, хорошим решением будет использование базы данных навыков и отправка RPC со строковыми параметрами вместо сериализуемых классов (ваш SkillEvent)

Таким образом, используя эту базу данных навыков, вы можете выполнять код со строковыми параметрами по своему желанию.

Вы можете поместить идентификатор навыка и параметры в строку, отправить rpc, получить команду, разобрать строки, выполнить код по идентификатору навыка с параметрами.

  1. Поместите параметры строки и идентификатор навыка в строку
  2. Отправить RPC с этой строкой
  3. Получить RPC
  4. Разобрать строку в ваш формат (в этом примере в jsonObject)
  5. Найти использованный навык id_skill и выполнить код с параметрами
  6. прибыль!

Полезным инструментом для разбора или создания строк будет какой-то анализатор JSON (serializator.. idk)

Может быть, вы спросите меня: "почему струны и JSON?"

Я отвечаю вам: "потому что это гибкий!"

Примеры:

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
}

Например, параметры для выполнения гранаты:

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

Или некоторые другие навыки:

// 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
}

И выполнить эти навыки:

[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
}

Где-то в вашей игре:

public class GrenadeSkill: BaseSkill{

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

Я надеюсь, вы понимаете мою идею.

Другие вопросы по тегам