Reputation: 69
I want to make a game where I have 3 types of characters, each type has a different attack for each other type. I would like to have an abstract class Character with an abstract method (it's a requirement) and 3 classes that inherit the method and then overload it for each type. The problem is that my attack method in my Character class takes a type Character argument and that I have to overwrite it in my sub classes with an empty method which makes the use of that abstract class and method really useless. So what can I do better than :
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*/
}
}
And so on for class B and C.
I would prefer to avoid that also :
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*/
}
}
}
I hope my question is clear enough despite my english.
Upvotes: 3
Views: 510
Reputation: 69
All those answers were very interesting, even if some of the concepts went over my head. Reading them I finally realized that there was no real problem (don't know why I created one but you know...), it's not really good looking but going for the simpliest solution I opted for the following :
I'll just consider the most common procedure and write it as the way to go for all classes that inherit from the abstract class. Then I can specify the methods for those that have a particular way of working the attack. So let's say that when a class A character has the same way of attacking a class A or B character but attacks differently characters of type C. Using polymorphism we just have to do :
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*/
}
}
The general method (the one that takes a character parameter) will be use for any character type object (or that inherits from character) except if it's a type C object/character; in this case it will be dispatched to the Attack method that takes a type C object/character as parameter.
And if all tree types are attacked a different way then we can just choose arbitrarily one of them to be implemented following the abstract method signature :
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*/
}
}
Well, I don't know if that's dumbing it down too much but it works. The usage of dynamic
, interface or generic types are probably good things but really there was nothing much to do here.
Thanks for the perspectives and please tell me if something isn't right.
To any fellow noob reading that I'd recommand to look at the elegant solution to a close problem (one that actually exists) presented in the section 3 of an article written by Eric Lippert on his blog that can be found following this link : https://ericlippert.com/2015/04/27/wizards-and-warriors-part-one/
Upvotes: 0
Reputation: 9704
If you want to avoid the dynamic dispatch solution dasblinkenlight has presented, you could go about this by implementing the Visitor pattern. We need to add a new method to your abstract class.
public abstract class Character
{
public int HP {get;set;}
public abstract void Attack(Character t);
public abstract void Accept(ICharacterVisitor visitor);
}
As well as create our ICharacterVisitor interface.
public interface ICharacterVisitor {
void Visit(A a);
void Visit(B b);
void Visit(C c);
}
Lets go ahead and define some rules for attacking as character A in the terms of the 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);
}
}
Now we can implement our A, B, and C characters. I've left the implementation of Attack on B and C as an exercise to the reader.
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();
}
}
Now, we can conduct attacks from characters on other characters fairly simply:
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
}
The logic is effectively decoupled from your object hierarchy. You can implement a DefaultAttackVisitor that goes through standard rules, or have an AttackVisitor for each character type, or perhaps some more complex rules based on weapons or spells. Either way you've ended up with a fairly clean and easily swapped set of logic for resolving attacks between classes.
Implementing visitor over using Dynamic Dispatch will lend you a bit more compile-time type safety. If at some point you were to add a new character type, D, you would be unable to compile without having made sure your visitor implementations were updated to include logic for D as soon as you implement D's Accept method.
With dynamic dispatch, your fall-through method would be hit for any newly added character types if the attacking type doesn't have, for instance, an added Attack(D d)
overload.
That said, dynamic dispatch has a much lower implementation overhead, but you give up some compile-time type safety. You'll have to assess which trade-off is more important for your use case.
Upvotes: 1
Reputation: 1279
Just use already built into C# typeof()
here is some example code.
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;
}
}
}
}
Upvotes: 0
Reputation: 726849
You can implement this dispatch with a little "magic" from 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"); }
}
Note the cast of both this
and c
to dynamic
, this is what lets the runtime locate the relevant DoAttack
override without relying on inheritance structure.
An advantage of this approach is that you can add new implementations as you wish: the rest of the code will continue to work, as long as attacks are limited to pairs of objects of "valid" types.
A disadvantage of this approach is that it is not statically typed, meaning that it would compile even in the absence of a method that handles the desired interaction. You can mitigate this problem by providing a "default" implementation of DoAttack(Character c)
in the Character
class itself.
Upvotes: 1
Reputation: 1
Since you know that you are only having 3 different types why dont you just create 3 abstract methods for each type in your Charackter class?
public abstract class Character
{
public abstract void Attack(A a);
public abstract void Attack(B a);
public abstract void Attack(C a);
}
Upvotes: -1