NOTE: This answer ended up being WAY longer than I expected, but I taylored it specifically to your RPG situation, so hopefully you find it helpful. It might be useful to actually copy the code into a Console application and play around with it to understand how it actually works.
Have you considered using interfaces in your design? I think they would really help a lot. I asked a question a week ago and got some great suggestions about using interfaces with Dependency Injection.
Using interfaces, you can avoid thinking "what containers of data do I need to perform this action?" and instead think "what action do I need?" Here's a simple example of how you might model your code to pull out the method of attacking in a battle:
Public Interface IAttacker
Function GetAttackDamage() As Double
Property Name As String
End Interface
Public Class Character
Implements IAttacker
Public Property Name As String Implements IAttacker.Name
Public Property Weapons As List(Of IWeapon)
Public Property SpecialTeamAttackMove As IWeapon
Public Function GetAttackDamage() As Double _
Implements IAttacker.GetAttackDamage()
'Get the sum of the damage in the weapons list'
Return Weapons.Select(Function(iw) iw.Damage).Sum()
End Function
End Class
Public Class Team
Implements IAttacker
Public Property Characters As List(Of Character) = _
New List(Of Character)()
Public Property Name As String Implements IAttacker.Name
Public Function GetAttackDamage() As Double _
Implements IAttacker.GetAttackDamage()
'Get the sum of the damage of the SpecialTeamAttackMove'
'of each character on the team'
Return Characters.Select(Function(c) _
c.SpecialTeamAttackMove.Damage).Sum())
End Function
End Public
Public Class Battle
Public Property AttackerA As IAttacker
Public Property AttackerB As IAttacker
Public Sub New(attackerA As IAttacker, attackerB As IAttacker)
Me.AttackerA = attackerA
Me.AttackerB = attackerB
End Sub
'This function returns the "winner" of the fight, unless it is a tie'
Public Function Fight() As IAttacker
If Me.AttackerA.GetAttackDamage() = Me.AttackerB.GetAttackDamage() Then
Return Nothing
ElseIf Me.AttackerA.GetAttackDamage() > _
Me.AttackerB.GetAttackDamage() Then
Return Me.AttackerA
Else
Return Me.AttackerB
End If
End Function
End Class
In the above code, the Battle
class only knows that it needs an object that implements IAttacker
. It doesn't care how that object implements GetAttackDamage()
, only that it can call the method when it needs to. The Name
property is also abstracted into the interface. See the following procedural examples of how this might work:
Public Sub BattleBetweenTwoCharacters()
'In this example, two characters are instantiated. Assume the "..." is where'
'you add weapons and other properties. The characters fight and the winning'
'IAttacker is returned'
Dim charA As Character = New Character() With {...}
Dim charB As Character = New Character() With {...}
Dim winningChar As IAttacker = New Battle(charA, charB).Fight()
If winningChar Is Nothing Then
Console.WriteLine("It was a tie.")
Else
Console.WriteLine("{0} was the winner",winningChar.Name)
End If
End Sub
Public Sub BattleBetweenTwoCharacters()
'In this example, several characters and a team are instantiated. '
'Assume the "..." is where you add weapons and other properties. A '
'fight takes place and the winner is returned.'
Dim charA As Character = New Character() With {...}
Dim charB As Character = New Character() With {...}
Dim charC As Character = New Character() With {...}
Dim teamAB As New Team()
teamAB.Characters.Add(charA)
teamAB.Characters.Add(charB)
'Here, a team fights a character. The Battle.Fight method will call '
'GetAttackDamage() on each object, even though a team implements'
'it differently than a single character.'
Dim winner As IAttacker = New Battle(teamAB, charB).Fight()
If winningChar Is Nothing Then
Console.WriteLine("It was a tie.")
Else
Console.WriteLine("{0} was the winner",winningChar.Name)
End If
End Sub
Hopefully you can see from this (woefully lengthy) example the advantage of using interfaces to help encapsulate specific functionality. This allows you to decouple the functionality you expect from a class from the class itself and allow the objects that consume that class to call the functionality it needs directly through the interface.
In the example I showed, the Battle
didn't care if it had a character. All it need to know about was that it had things that could attack, that is, things which implemented the IAttacker
interface. Because we wrote it this way, we weren't limiting our battles to just Character
s. We are also giving the Battle
object only the things necessary to fight. The Battle
doesn't necessarily need to know about all of the experience points, items, pets, etc. associated with an attacker. The only thing it cares about is that when it asks for the total damage that an attacker can give, the attacker provides that information. Each IAttacker
might implement it differently, but that's OK, as long as the Battle
object gets the information it needs.
If you mix interfaces with base classes in your design, you'll be off to a very good start in structuring your class hierarchy.