Skip to main content

MovableObjectBase

Written by: Akram Taghavi-Burris | © Copyright 2024
Status

MovableObjectBase Abstract

While interfaces can be implemented by any class, in this instance have choose to implement the IMovable interface through an abstract class.

In our collectible game, using MovableObjectBase abstract class allows us to implement the IMovable interface while defining common functionality.

Interface Requirements

The IMovable interface defines the contract for all movable objects, meaning any class implementing this interface must provide the properties and methods defined in the interface. These requirement include:

  • Speed and Direction Properties: In the IMovable interface, Speed and Direction are defined as public properties with getters and setters, ensuring that any class implementing the interface allows access to these values. However, to maintain encapsulation in the MovableObjectBase abstract class, we define the private fields _speed and _direction. These fields store the actual values, while the public properties manage controlled access to them. This way, we can control how speed and direction are modified or retrieved, ensuring consistency and allowing for additional logic (such as validation) if needed.

  • Move() Method: This required method implements the behavior for moving the object. A common approach to moving objects is by adjusting their position based on speed and direction, for example: transform.position += _direction * _speed * Time.deltaTime;. While specific movement behaviors may vary depending on the use case, this simple calculation serves as a reliable default implementation. It ensures the object moves smoothly in the specified direction at a speed that is consistent with the game's frame rate.

  • Stop() Method: There are various ways to stop a movable object, depending on the desired behavior. To maintain flexibility, we have left the Stop() method empty in the base class. This allows each concrete subclass to provide its own specific implementation based on its needs.

  • Reset() Method The purpose of this method is to reset the object to its original position, speed, and direction. To accomplish this, we need to store the initial values for these properties as _defaultSpeed, _defaultDirection, and _defaultPosition.

    • Start() Method: The best place to record the defaults values is in the Start() method, as it takes place before the first frame but after the Awake (initialization).
    • SetDefaults() Method: Instead of directly setting these values in the Start() method, we have opted to call a SetDefaults() method within Start(). This separates the logic for recording defaults, keeping the code organized and allowing for easy reuse if the defaults need to be recalculated later.
Overriding Methods

A method defined as virtual allows subclasses to override the method using the override definition. In this instance, the subclass can modify the method by including a call to the original method using base.MethodName(); before or after adding its own behavior. This approach enables you to extend the functionality of the original method while still respecting its original intent.

Additionally, you can define a method as abstract with in a base class. Unlike virtual methods, abstract methods do not provide default content; instead, they require the child class to implement their own versions. This ensures that any subclass must define specific behavior, promoting a consistent interface while allowing for unique implementations tailored to the needs of each subclass.

Since the way an object moves may differ depending on its type, we plan to define the required methods as virtual methods. By doing this, we allow subclasses to override these methods, providing the flexibility to handle various movement scenarios while maintaining a consistent interface.

MovableObjectBase.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public abstract class MovableObjectBase : MonoBehaviour, IMovable
{
private float _speed = 5f; // Speed of the object
private Vector3 _direction = Vector3.right; // Direction of movement

// Default values for Reset
private float _defaultSpeed;
private Vector3 _defaultPosition;
private Vector3 _defaultDirection;

// Property to get or set the speed of the object
public float Speed
{
get => _speed;
set => _speed = value;
}

// Property to get or set the direction of movement for the object
public Vector3 Direction
{
get => _direction;
set => _direction = value;
}

//Start called before the first frame
protected virtual void Start()
{
SetDefaults(); //set the defaults

}//end Start()


//Set the default values
private void SetDefaults()
{
_defaultSpeed = _speed;
_defaultDirection = _direction;
_defaultPosition = transform.position;
}//end SetDefulats()

// Method to move the object
public virtual void Move()
{
transform.position += _direction * _speed * Time.deltaTime; //default movement
}//end Move()

// Method to reverse the current direction of the object
public virtual void ReverseDirection()
{
_direction = -_direction;
}//end ReverseDirection()

// Method to stop the objects movement
public virtual void Stop()
{
// Logic for stopping (e.g., set speed to 0)
}//end Stop()

// Method to rest the object
public virtual void Reset()
{
// Reset position, speed, direction
transform.position = _defaultPosition; // Example reset logic
_speed = _defaultSpeed;
_direction = _defaultDirection;
}//end Reset()

}

Access in the Unity Editor

When designing movable objects for a game, a level designers will most likely need the ability to set certain properties such as speed and direction in the Unity Editor. To accommodate this work flow, we can use [SerializeField] attributes to make these properties accessible while still maintaining encapsulation. The highlighted code below demonstrates how the [SeralizedField] attribute can be used in Base Class and made accessible to all sub classes.

MovableObjectBase.cs Serialized Fields
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public abstract class MovableObjectBase : MonoBehaviour, IMovable
{
[SerializeField] private float _speed = 5f; // Speed of the object
[SerializeField] private Vector3 _direction = Vector3.right; // Direction of movement

// Default values for Reset
private float _defaultSpeed;
private Vector3 _defaultPosition;
private Vector3 _defaultDirection;

//....continue

Further Abstractions

Since direction is closely linked to how a subclass moves, having a method in the base class to set direction can enhance functionality, even when a public property exists. Some examples include:

  • Validate or Constrain: Check for valid values (e.g., ensuring the direction isn't zero) when setting direction.

  • Log or Debug: Use to encapsulate logging or tracking changes in direction.

  • Implement Complex Logic: Recalculate velocity, update animation states, and more within a single method call.

  • Subclass Customization: Allow subclasses to override how direction is set while still calling base-class logic.

  • Consistency Across Multiple Properties: Handle interdependencies between properties like speed or acceleration.

  • Encapsulate Implementation Details: Hide the implementation details of setting direction, allowing changes without affecting external code.

Implementing Direction Management

With the need for flexibility and customization in how movable objects manage their direction, we can abstract this logic into reusable methods. This ensures that the base functionality is robust, while allowing subclasses to extend or modify behavior as needed. To achieve this, we will introduce two key methods:

  • SetDirection() Method: This protected, virtual method is responsible for assigning a new direction to the object. It takes a Vector3 parameter representing the direction and ensures that the base class logic for setting it is applied consistently. Subclasses can override this method to customize how the direction is set while still preserving the core functionality.

  • CalculateDirection() Method: While setting a direction may often be straightforward, there are cases where calculating the direction requires additional logic. This protected, virtual method is designed to handle any calculations needed to determine the correct direction before it's set. By default, it simply returns the current direction, but subclasses can override this method to implement more complex behaviors, such as accounting for obstacles, pathfinding, or other gameplay mechanics. This ensures the right direction is computed before calling SetDirection().

By separating the concerns of setting and calculating the direction, this approach adheres to the single responsibility principle, making the logic easier to maintain and extend.

The highlighted line below demonstrates how direction management is implemented into the current MoveableObjectBaseclass.

MovableObjectBase.cs Implementing Direction Management
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public abstract class MovableObjectBase : MonoBehaviour, IMovable
{
[SerializeField] private float _speed = 5f; // Speed of the object
[SerializeField] private Vector3 _direction = Vector3.right; // Direction of movement

// Default values for Reset
private float _defaultSpeed;
private Vector3 _defaultPosition;
private Vector3 _defaultDirection;

// Property to get or set the speed of the object
public float Speed
{
get => _speed;
set => _speed = value;
}

// Property to get or set the direction of movement for the object
public Vector3 Direction
{
get => _direction;
set => _direction = value;
}

//Start called before the first frame
protected virtual void Start()
{
SetDefaults(); //set the defaults

}//end Start()

//Set the default values
private void SetDefaults()
{
_defaultSpeed = _speed;
_defaultDirection = _direction;
_defaultPosition = transform.position;
}//end SetDefulats()

// Protected method to set direction in derived classes
protected void SetDirection(Vector3 direction)
{
_direction = direction;
}//end SetDirection()

// Virtual method for subclasses to implement their direction calculation logic
protected virtual Vector3 CalculateDirection()
{
return _direction;
}//end CalculateDirection()

// Method to move the object
public virtual void Move()
{
transform.position += _direction * _speed * Time.deltaTime; //default movement
}//end Move()

// Method to reverse the current direction of the object
public virtual void ReverseDirection()
{
_direction = -_direction;
}//end ReverseDirection()

// Method to stop the objects movement
public virtual void Stop()
{
// Logic for stopping (e.g., set speed to 0)
}//end Stop()

// Method to rest the object
public virtual void Reset()
{
// Reset position, speed, direction
transform.position = _defaultPosition; // Example reset logic
_speed = _defaultSpeed;
_direction = _defaultDirection;
}//end Reset()

}

State Pattern

To enhance the functionality of our MovableObjectBase class, we can introduce state management using the State Pattern. The State Pattern allows an object to change its behavior when its internal state changes, enabling us to encapsulate state-specific behavior in a clean and organized manner.

Why Use the State Pattern?

  • Simplified Code Maintenance: By isolating state-specific logic into distinct state classes, we reduce complexity within the MovableObjectBase class, making it easier to maintain and extend.

  • Enhanced Readability: The behavior associated with each state can be clearly defined in its own class, leading to more readable code.

  • Flexible State Transitions: The State Pattern allows for dynamic transitions between states, facilitating smoother changes in behavior as the object's conditions change.

States in MovableObjectBase

To implement state management, we will define an enum, MovableObjectStates, representing the various states of our movable objects.

States in programming can be defined as their own separate class or as enums within another class, depending on their intended use and scope. Defining states as separate classes can enhance reusability and clarity, especially if multiple classes require similar state management or if the states may evolve in complexity over time. This approach promotes modular design and allows for easier maintenance.

In this example we anticipate the following states:

  • Idle: Used when the object is initialized in the Awake() method, but not yet moving, awaiting a trigger or input to begin motion.
  • Moving: Used when the object is actively moving, updating its position based on speed and direction. In this instance the moving state will be set in the Start() right before the first frame.
  • Stopped: Used when the object has stopped completely, with no motion, until explicitly instructed to move again or reset.

Conversely, defining states as enums within a specific class, such as the MovableObjectBase, can streamline the code and enhance encapsulation when the states are only relevant to that class.

In our case, we chose to place the state management within the MovableObjectBase class because it directly controls the behaviors of movable objects. This decision allows us to keep related functionality together, simplifying code readability and maintenance while ensuring that state transitions are tightly coupled with the object's movement logic.

Tracking and Managing State Transitions

To manage transitions between states, we will use a private variable _currentState to track the current state of the object. Encapsulating this state information ensures that only the MovableObjectBase class knows which state the object is in, and external components do not directly access or manipulate it. Instead, the methods within the class, such as Move(), Stop(), and Reset(), will update the state internally.

By keeping the state management private, we maintain encapsulation, which reduces the risk of unintended modifications and enhances the maintainability of the code.

State Changes in Update()

The Update() method will continuously check the current state and call the appropriate behavior based on it. For example, when the object is in the Moving state, Update() will call the Move() method to update the position. Similarly, if the state is Stopped, no movement will occur.

Each designated method will also be responsible for transitioning to the appropriate state:

  • Move() will set the state to Moving.
  • Stop() will set the state to Stopped.
  • Reset() will return the object to its default values (position, speed, and direction) and set the state back to Moving.

This structure allows us to easily manage and extend behavior for the object as it changes states, keeping the logic modular and easy to maintain.

Implementing States

The highlighted code below demonstrates how the State Pattern is implemented in the MovableObjectBase class.

MovableObjectBase.cs Adding States
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MovableObjectBase : MonoBehaviour, IMovable
{
public enum MovableObjectStates
{
Idle,
Moving,
Stopped
}

[SerializeField] private float _speed = 5f; // Speed of the object
[SerializeField] private Vector3 _direction = Vector3.right; // Direction of movement


// Default values for Reset
private float _defaultSpeed;
private Vector3 _defaultPosition;
private Vector3 _defaultDirection;

// State management
private MovableObjectStates _currentState;

// Property to get or set the speed of the object
public float Speed
{
get => _speed;
set => _speed = value;
}

// Property to get or set the direction of movement for the object
public Vector3 Direction
{
get => _direction;
set => _direction = value;
}

protected virtual void Awake()
{
_currentState = MovableObjectStates.Idle; // set initial state
}//end Awake()


//Start called before the first frame
protected virtual void Start()
{
SetDefaults(); //set the default values for reseting movables

_currentState = MovableObjectStates.Moving; // set the state to moving

}//end Start()

//Update called every frame, checks for state
protected virtual void Update()
{
switch (_currentState)
{
case MovableObjectStates.Idle:
break;
case MovableObjectStates.Moving:
Move();
break;
case MovableObjectStates.Stopped:
Stop();
break;
}//end state switch

}//end Update()

//Set the default values
private void SetDefaults()
{
_defaultSpeed = _speed;
_defaultDirection = _direction;
_defaultPosition = transform.position;
}//end SetDefulats()

// Protected method to set direction in derived classes
protected void SetDirection(Vector3 direction)
{
_direction = direction;
}//end SetDirection()

// Virtual method for subclasses to implement their direction calculation logic
protected virtual Vector3 CalculateDirection()
{
return _direction;
}//end CalculateDirection()

// Method to move the object
public virtual void Move()
{
_currentState = MovableObjectStates.Moving; //set the state to Moving

transform.position += _direction * _speed * Time.deltaTime; //default movement
}//end Move()

// Method to reverse the current direction of the object
public virtual void ReverseDirection()
{
_direction = -_direction;
}//end ReverseDirection()

// Method to stop the objects movement
public virtual void Stop()
{
_currentState = MovableObjectStates.Stopped; //set the state to Stopped

// Logic for stopping (e.g., set speed to 0)
}//end Stop()

// Method to rest the object
public virtual void Reset()
{
// Reset position, speed, direction
transform.position = _defaultPosition; // Example reset logic
_speed = _defaultSpeed;
_direction = _defaultDirection;

// Immediately start moving again
_currentState = MovableObjectStates.Moving;
}//end Reset()

}

Moving Into Infinity

While our MovableObjectBase handles basic movement, it doesn't yet include functionality to prevent objects from moving endlessly off-screen or beyond defined boundaries. Although the class has a ReverseDirection() method, it's never called, leaving subclasses vulnerable to moving into infinity without any constraints.

To avoid adding this logic directly into the base class, which would violate the Single Responsibility Principle, we delegate boundary management to a dedicated Boundary class. This class will handle all boundary checks, ensuring objects stay within predefined limits. By using the Observer Pattern, the MovableObjectBase can listen for boundary events and respond appropriately, like reversing direction, without managing those checks directly. This approach ensures both flexibility and clean, maintainable code.

In Summary

The MovableObjectBase class establishes a solid foundation for creating movable game objects. This class implements the IMovable interface, ensuring a consistent contract for movement behavior across different subclasses.

Key Features

  • State Management: The use of states like Idle, Moving, and Stopped facilitates clear control over object behavior.
  • Encapsulation of Movement: Properties for speed and direction allow for customization while adhering to the Single Responsibility Principle.
  • Flexible Direction Management: Methods like SetDirection() and CalculateDirection() enable subclasses to define movement direction effectively.
  • Reset Functionality: The Reset() method supports easy restoration of default values.

Moving forward, we will delve into the CustomMover class, which serves as a concrete implementation of the MovableObjectBase, allowing for tailored movement behaviors and enhancing the gameplay experience.

.