Я хочу создать игру, в которой у меня есть 3 типа персонажей, у каждого типа есть своя атака для другого типа. Я хотел бы иметь абстрактный класс Character с абстрактным методом (это требование) и 3 класса, которые наследуют метод, а затем перегружают его для каждого типа. Проблема в том, что мой метод атаки в моем классе Character принимает аргумент типа Character, и мне приходится перезаписывать его в своих подклассах пустым методом, что делает использование этого абстрактного класса и метода действительно бесполезным. Итак, что я могу сделать лучше, чем:
public abstract class Character
{
public abstract void Attack(Character t);
}
public class A :Character
{
public override void Attack(Character t){}
public void Attack(A x)
{
/*instructions*/
}
public void Attack(B y)
{
/*instructions*/
}
public void Attack(C z)
{
/*instructions*/
}
}
И так далее для классов B и C.
Я бы также предпочел избежать этого:
public abstract class Character
{
public abstract void Attack(Character c);
}
public class A :Character
{
public override void Attack(Character t)
{
A x = t as A
if (x != null)
{
/*instructions*/
}
B y = t as B
if (y != null)
{
/*instructions*/
}
C z = t as C
if (z != null)
{
/*instructions*/
}
}
}
Надеюсь, мой вопрос достаточно ясен, несмотря на мой английский.
Мне не имеет смысла помещать инструкции для всех 3 классов в A.
Я написал серию статей об этой проблеме; вам, вероятно, будет интересно. ericlippert.com/2015/04/27/wizards-and-warriors-part-one. (Прочтите все; ваша конкретная проблема начинается в части 3.) Комментарий Джонатона Чейза верен; проблема, с которой вы столкнулись, заключается в том, что C# отправляется однократно. Я считаю, что вам не следует в первую очередь пытаться представлять правила игры в системе типов! Создайте класс, представляющий правила игры, и поместите правила в что.





Поскольку вы знаете, что у вас есть только 3 разных типа, почему бы вам просто не создать 3 абстрактных метода для каждого типа в вашем классе Charackter?
public abstract class Character
{
public abstract void Attack(A a);
public abstract void Attack(B a);
public abstract void Attack(C a);
}
Каждому классу нужно будет переопределить все.
@paparazzo, я думал, это то, что он хотел. у каждого персонажа есть функция для каждого другого персонажа
Зачем вам 3 класса, если все делают все? Это был не мой голос против.
Вы можете реализовать эту рассылку с помощью небольшой "магии" от dynamic:
abstract class Character {
public void Attack(Character c) {
((dynamic)this).DoAttack((dynamic)c);
}
}
class A : Character {
public void DoAttack(A a) { Console.WriteLine("A attacks A"); }
public void DoAttack(B b) { Console.WriteLine("A attacks B"); }
public void DoAttack(C c) { Console.WriteLine("A attacks C"); }
}
class B : Character {
public void DoAttack(A a) { Console.WriteLine("B attacks A"); }
public void DoAttack(B b) { Console.WriteLine("B attacks B"); }
public void DoAttack(C c) { Console.WriteLine("B attacks C"); }
}
class C : Character {
public void DoAttack(A a) { Console.WriteLine("C attacks A"); }
public void DoAttack(B b) { Console.WriteLine("C attacks B"); }
public void DoAttack(C c) { Console.WriteLine("C attacks C"); }
}
Обратите внимание на преобразование this и c в dynamic, это то, что позволяет среде выполнения находить соответствующее переопределение DoAttack, не полагаясь на структуру наследования.
Преимущество этого подхода состоит в том, что вы можете добавлять новые реализации по своему усмотрению: остальной код будет продолжать работать, пока атаки ограничиваются парами объектов «допустимых» типов.
Недостатком этого подхода является то, что он не имеет статической типизации, что означает, что он будет компилироваться даже при отсутствии метода, который обрабатывает желаемое взаимодействие. Вы можете смягчить эту проблему, предоставив реализацию DoAttack(Character c) "по умолчанию" в самом классе Character.
Если вы хотите избежать решения динамической диспетчеризации, представленного dasblinkenlight, вы можете сделать это, реализовав шаблон Visitor. Нам нужно добавить новый метод в ваш абстрактный класс.
public abstract class Character
{
public int HP {get;set;}
public abstract void Attack(Character t);
public abstract void Accept(ICharacterVisitor visitor);
}
А также создадим наш интерфейс ICharacterVisitor.
public interface ICharacterVisitor {
void Visit(A a);
void Visit(B b);
void Visit(C c);
}
Давайте продолжим и определим некоторые правила для атаки как персонаж A в терминах ICharacterVisitor.
public class AttackVisitorA : ICharacterVisitor
{
private int _baseDamage;
public AttackVisitorA(int baseDamage){
_baseDamage = baseDamage;
}
public void Visit(A a)
{
// A does normal damage to A.
a.HP -= _baseDamage;
}
public void Visit(B b)
{
// A does double damage to B.
b.HP -= (_baseDamage * 2);
}
public void Visit(C c)
{
// A does half damage to C.
c.HP -= (_baseDamage / 2);
}
}
Теперь мы можем реализовать наши символы A, B и C. Я оставил реализацию атаки на B и C в качестве упражнения для читателя.
public class A : Character
{
public override void Accept(ICharacterVisitor visitor)
{
visitor.Visit(this);
}
public override void Attack(Character t)
{
var damage = 15;
t.Accept(new AttackVisitorA(damage));
}
}
public class B : Character
{
public override void Accept(ICharacterVisitor visitor)
{
visitor.Visit(this);
}
public override void Attack(Character t)
{
throw new NotImplementedException();
}
}
public class C : Character
{
public override void Accept(ICharacterVisitor visitor)
{
visitor.Visit(this);
}
public override void Attack(Character t)
{
throw new NotImplementedException();
}
}
Теперь мы можем довольно просто проводить атаки персонажей на других персонажей:
void Main()
{
var a = new A { HP = 100 };
var b = new B { HP = 100 };
var c = new C { HP = 100 };
a.Attack(a); // stop hitting yourself
a.Attack(b);
a.Attack(c);
Console.WriteLine(a.HP); // 85
Console.WriteLine(b.HP); // 70
Console.WriteLine(c.HP); // 93
}
Логика эффективно отделена от иерархии объектов. Вы можете реализовать DefaultAttackVisitor, который проходит через стандартные правила, или иметь AttackVisitor для каждого типа персонажа, или, возможно, некоторые более сложные правила, основанные на оружии или заклинаниях. В любом случае у вас получился довольно чистый и легко заменяемый набор логики для разрешения атак между классами.
Реализация посетителя вместо использования Dynamic Dispatch предоставит вам немного больше безопасности типов во время компиляции. Если в какой-то момент вам нужно будет добавить новый тип символа, D, вы не сможете скомпилировать, не убедившись, что ваши реализации посетителя были обновлены для включения логики для D, как только вы реализуете метод D Accept.
При динамической отправке ваш метод провала будет задействован для любых вновь добавленных типов символов, если атакующий тип не имеет, например, добавленной перегрузки Attack(D d).
Тем не менее, динамическая отправка требует гораздо меньших затрат на реализацию, но вы отказываетесь от некоторой безопасности типов во время компиляции. Вам нужно будет оценить, какой компромисс более важен для вашего варианта использования.
Просто используйте уже встроенный в C# typeof(), вот пример кода.
public class Program
{
public static void Main()
{
A a = new A();
B b = new B();
a.Attack(b);
b.Attack(a);
Console.WriteLine(typeof(A));
Console.WriteLine(typeof(B));
Console.WriteLine(typeof(A) == a.GetType());
Console.WriteLine(typeof(B) == a.GetType());
Console.ReadLine();
}
public abstract class Character
{
public abstract void Attack(Character c);
}
public class A : Character
{
public override void Attack(Character t)
{
if (t.GetType() == typeof(A))
{
Console.WriteLine("A attacked type A");
return;
}
if (t.GetType() == typeof(B))
{
Console.WriteLine("A attacked type B");
return;
}
if (t.GetType() == typeof(C))
{
Console.WriteLine("A attacked type C");
return;
}
}
}
public class B : Character
{
public override void Attack(Character t)
{
if (t.GetType() == typeof(A))
{
Console.WriteLine("B attacked type A");
return;
}
if (t.GetType() == typeof(B))
{
Console.WriteLine("B attacked type B");
return;
}
if (t.GetType() == typeof(C))
{
Console.WriteLine("B attacked type C");
return;
}
}
}
public class C : Character
{
public override void Attack(Character t)
{
if (t.GetType() == typeof(A))
{
Console.WriteLine("C attacked type A");
return;
}
if (t.GetType() == typeof(B))
{
Console.WriteLine("C attacked type B");
return;
}
if (t.GetType() == typeof(C))
{
Console.WriteLine("C attacked type C");
return;
}
}
}
}
Я считаю, что OP старался избежать именно такой реализации.
@JonathonChase Я думаю, из вопроса, и я могу ошибаться, что OP искал способ реализовать одно переопределение для каждого класса, которое могло бы делать разные вещи в зависимости от класса, который подвергался атаке, и злоумышленника. Скажем, если класс C атакует B, он нанесет 50 повреждений, но если A атакует B, он нанесет только 25 и так далее. По крайней мере, так я интерпретировал это после того, как прочитал и увидел желаемый тип кода из второго примера.
Все эти ответы были очень интересными, даже несмотря на то, что некоторые концепции были мне не по зубам. Прочитав их, я наконец понял, что реальной проблемы не было (не знаю, зачем я ее создал, но вы знаете ...), это не очень хорошо выглядит, но я выбрал самое простое решение, которое я выбрал следующее:
Я просто рассмотрю наиболее распространенную процедуру и напишу ее как подходящую для всех классов, унаследованных от абстрактного класса. Затем я могу указать методы для тех, у кого есть конкретный способ работы с атакой. Итак, предположим, что когда персонаж класса A имеет один и тот же способ атаковать символ класса A или B, но атакует по-разному символы типа C. Используя полиморфизм, нам просто нужно сделать:
public abstract class Character
{
public abstract void Attack(Character t);
}
public class A :Character
{
public override void Attack(Character t)
{
/*instructions for the attacking of a character of type A or B*/
}
public void Attack(C z)
{
/*instructions for the attacking of a type C character*/
}
}
Общий метод (тот, который принимает символьный параметр) будет использоваться для любого объекта символьного типа (или наследуемого от символа), кроме того, если это объект / символ типа C; в этом случае он будет отправлен методу Attack, который принимает объект / символ типа C в качестве параметра.
И если все типы деревьев атакованы по-разному, мы можем просто произвольно выбрать один из них для реализации, следуя сигнатуре абстрактного метода:
public abstract class Character
{
public abstract void Attack(Character t);
}
public class A :Character
{
public override void Attack(Character t)
{
/*instructions for the attacking of a character of type A*/
}
public void Attack(B y)
{
/*instructions for the attacking of a type B character*/
}
public void Attack(C z)
{
/*instructions for the attacking of a type C character*/
}
}
Ну, я не знаю, слишком ли это приглушает, но это работает. Использование dynamic, интерфейса или универсальных типов, вероятно, хорошо, но на самом деле здесь особо нечего было делать.
Спасибо за перспективы и, пожалуйста, скажите мне, если что-то не так.
Любому читателю-новичку, который я бы порекомендовал взглянуть на элегантное решение близкой проблемы (которая действительно существует), представленное в разделе 3 статьи, написанной Эриком Липпертом в его блоге, которую можно найти по этой ссылке: https://ericlippert.com/2015/04/27/wizards-and-warriors-part-one/
Если A, B и C являются производными от Character, вы столкнетесь с проблемами, связанными с отправкой C# по отдельности. Скорее всего, вы смотрите на реализацию паттерна Посетитель, чтобы выполнить то, что вы хотите.