- Home /
How do I correctly combine behaviors on a base class?
Hi there--I'm pretty new to OOP, trying to figure some stuff out. Any help is much appreciated.
I'm working on a simple overhead 2d game. I want to create enemies that move around the screen in a variety of ways. There are two major qualities that each enemy has: how it moves, and how it reacts at the edge of the screen.
Some enemies move in straight lines, some enemies move in sine waves, etc. Some enemies bounce off the edge of the screen, Pong-style; others, when they hit the edge, get repositioned to the opposite edge, looping merrily along their way.
I have a BaseEnemy class that holds their HP, their X and Y velocities, etc. I've successfully coded two child scripts, Ponger and Looper, that derive from BaseEnemy, that control how the enemy reacts to screen edges.
I now want to create a different set of scripts that control how the enemy moves. Call them Drifter and Siner. One moves steadily in one direction; the other moves in a sine wave.
On its own, this would be pretty easy. What I can't figure out is how to correctly set up the movement scripts to interact with the edge scripts. If both edge scripts and movement scripts inherit from BaseEnemy, and I add them both to an object, one simply stops working, and I can see in the inspector that each of them has its own versions of the BaseEnemy's variables (HP etc). So that's no go.
If one (either edge script or movement script) is a MonoBehavior, it doesn't have access to the base class's variables (velocities etc.) I can maybe do a GetComponent to get that access (? maybe?) but that doesn't seem like the right thing.
Third option is to create hybrid scripts derived from the BaseEnemy class, called LooperDrifter, LooperSiner, PongerDrifter, PongerSiner. This obviously gets unwieldy as I add more behaviors, and is pretty clearly not the way OOP is supposed to be.
Fourthly, I could put the edge behaviors into the base class. That probably would work in this case since I don't imagine ending up with more than 3-4 edge behaviors. But that doesn't seem philosophically right either.
Could somebody help me understand what the right way to do this is? Thanks for your time!
Here's a simplified version of BaseEnemy:
public class BaseEnemy : MonoBehaviour {
protected float xSpeed;
protected float ySpeed;
protected int HP;
public virtual void Start () {
}
public virtual void Move () {
transform.Translate(new Vector3(xSpeed, ySpeed, 0) * Time.deltaTime);
}
public virtual void Update () {
Move();
}
}
Here's a simplified version of Ponger:
public class Ponger : BaseEnemy {
public virtual void Start () {
base.Start();
}
public virtual void Update () {
base.Update ();
// this is what makes it a Ponger
if ((transform.position.x < -10)||(transform.position.x > 10)) {
xSpeed = -xSpeed;
}
if ((transform.position.y < -10)||(transform.position.y > 10)) {
ySpeed = -ySpeed;
}
}
}
Here's a simplified version of Drifter:
public class Drifter : BaseEnemy {
void Start () {
xSpeed = Random.Range(-10,10);
ySpeed = Random.Range(-10,10);
}
void Update () {
base.Update();
}
}
Very thorough and well thought out question, so +1 for that, along with actually trying to figure it out yourself. Now, for us to help you even better, it'd be great if we could get some code that you are using so we can see if things are interfering with each other.
Answer by Jamora · Oct 07, 2013 at 06:19 AM
In normal software development you would use interfaces. This question was similar, a slightly simpler case where deep inheritance was the answer. In my opinion, Unity has a little bit different take on interfaces which I'll explain as my last option.
This particular problem is easily solved with the use of interfaces like I explained in the other answer.
Basically, you create interfaces for each edge behavior and move behavior. You can access them in code with this method, or something similar.
What I have in mind is something in lines of:
public interface IEdgeReacter{
void ReactToEdge();
}
public interface IMover{
void Move();
}
public class Ponger : BaseEnemy, IMover, IEdgeReacter{
public void ReactToEdge(){
//Ponger edge reation implementation
}
public void Move(){
transform.Translate(new Vector3(xSpeed, ySpeed, 0) * Time.deltaTime);
// this is what makes it a Ponger
if ((transform.position.x < -10)||(transform.position.x > 10)) {
xSpeed = -xSpeed;
}
if ((transform.position.y < -10)||(transform.position.y > 10)) {
ySpeed = -ySpeed;
}
}
}
public class Drifter : BaseEnemy, IMover, IEdgeReacter{
public void ReactToEdge(){
//Drifter edge reation implementation
}
public void Move(){
//Drifter specific move implementation
}
}
If your enemies all have the same behavior, then using interfaces might be overkill. In case they all have the same behaviors, but only different implementation you can just have either an abstract base class (my preferred way) or have virtual functions in the base class like you have now. The advantage in using this over interfaces, is you can encapsulate the behavior in the classes using the protected keyword. That means only the class and its children have access to that method.
The idea is you have all behavior in the base class, and all implementation in the extended classes:
public abstract class BaseEnemy{
protected abstract void Move();
protected abstract void ReactToEdge();
protected void Update(){
//Implementation here to detect edges
Move ();
}
}
public class Ponger : BaseEnemy{
protected void Move(){
transform.Translate(new Vector3(xSpeed, ySpeed, 0) * Time.deltaTime);
// this is what makes it a Ponger
if ((transform.position.x < -10)||(transform.position.x > 10)) {
xSpeed = -xSpeed;
}
if ((transform.position.y < -10)||(transform.position.y > 10)) {
ySpeed = -ySpeed;
}
}
protected override void ReactToEdge(){
//Ponger specific Edge reaction.
}
void Update(){
base.Update();
}
}
public class Drifter : BaseEnemy{
protected override void Move ()
{
//Drifter specific Movement
}
protected override void ReactToEdge(){
//Drifter specific Edge reaction.
}
void Update(){
base.Update();
}
}
EDIT:
A third method, possibly the most-Unity way, which I think you were after is to have one behavior per script. To create a ponger, you would have an Enemy
, a StraightLineMover
and an EdgeBouncer
script attached to a prefab named "Ponger". To create a drifter, you would have an Enemy
, a StraightLineMover
and a LoopOnEdge
script in a prefab named "Drifter". You can then combine these behaviors as you see fit. This whole process bears a striking similarity to the first, interface method.
How to use all these scripts then? You will need to apply the Command pattern. Have a (possibly abstract) base class EnemyMover
which contains an abstract Move() method. This script is inherited by StraightLineMover
, which means you can then have a
[SerializeField]
private EnemyMover moverScript;
//alternatively public without SerializeField
in your Enemy
script which you can then assign in the inspector to point to the StraightLineMover
Component. In the Update of Enemy
you can merely call moverScript.Move();
and the right movement will be executed, provided you set the prefab connections correctly. This would work even if you had a Component SineWaveMover
, as long as it extends EnemyMover.
Whoa! That's an epic answer. I'm going to have to dig into this (it sounds like approach 3 might be the way to go), but thank you so much for taking the time to offer such a thorough response. I really appreciate it!
Your answer
Follow this Question
Related Questions
Inheritance from object to object 1 Answer
How do I write this C# snipet in java script? 1 Answer
Problem using variable of type CustomStat to indicate which player statistic to change 1 Answer
FPS weapons class structure 1 Answer
Calling a method of a class that is a part of another class? 1 Answer