iTranslated by AI
My First Refactoring Experience: Incorporating ScriptableObject
Until now, I had packed all the player behaviors into a single giant class (Playeroperate.cs).
However, as development progressed, the following problems emerged:
- The class kept bloating, and the impact of every feature addition or specification change exploded.
- I often felt lost, wondering "What is running where?", making it difficult to identify the cause of bugs.
Judging this as a bad situation, I challenged myself to refactor while keeping the Single Responsibility Principle (SRP) in mind.
SRP is a design principle stating that "a class should have only one responsibility," and it is said that following this significantly improves maintainability and extensibility.
By the way, as a result of refactoring code that was about 300 lines long,
I ended up having to rewrite about 1,200 lines in the end...

Issues before refactoring: Structure of the Playeroperate class
The original Playeroperate class had the following characteristics.
public class Playeroperate : MonoBehaviour
{
public float PlayerHp = 100f;
[SerializeField] private float flywindHorizontalAmount = 3f;
[SerializeField] private bool IsGround = false;
[SerializeField] private bool IsJump = false;
//... (Numerous parameters for movement)
//... (Numerous bools for color management)
//... (Attack, damage, and color switching are all processed here)
void Update()
{
GroundYposCheck();
if (talksystem.isTalking) return;
changeColor();
CheckAttack();
}
void FixedUpdate()
{
if (talksystem.isTalking) return;
Move();
}
private void Move()
{
if (ColorPlayer == PlayerColorState.Red)
HandleMovementForColor(MaxRedSpeed, RedAddRunforceply, Defaultgravity, RedJumpSpeed);
else if (ColorPlayer == PlayerColorState.Green)
HandleMovementForColor(MaxGreenSpeed, GreemAddRunforceply, Lightgravity, GreenJumpSpeed);
else if (ColorPlayer == PlayerColorState.Blue)
HandleMovementForColor(MaxBlueSpeed, BlueAddRunforceply, Defaultgravity, BlueJumpSpeed);
}
// In addition, a vast number of methods such as attack processing, color change processing, floor detection, and damage processing are concentrated in one class.
}
Furthermore, what's even worse are the serialized variables.

As you can see, it's quite a mess.
Summary of issues
- Multiple responsibilities such as movement, jumping, attack, color switching, state management, and HP management are mixed together.
- Color states are managed by multiple bool variables, making the intention of state transitions unclear.
- Input detection is done directly, and conditional branches are concentrated in Update and FixedUpdate.
- The amount of code is large, making it difficult to grasp a single class.
- A catastrophic amount of variables.
Design Policy after refactoring
- Divide responsibilities and limit the role of each class.
- Input management by InputManager, input analysis/conditional branching by PlayerInputHandler.
- Movement by PlayerMove, color state management by PlayerColorManager.
- Attack by PlayerAttack, damage and HP management by PlayerStateManager.
- Centralized management of parameters for each color using ScriptableObject.
- Clarify state management with enums or flags, avoiding management with numerous bools.
- Keep classes as loosely coupled as possible, passing necessary information through clear APIs.
- Manage HP, speed, etc., with ScriptableObject and avoid creating variables for each color.
Specific Code Example: ScriptableObject
using UnityEngine;
[CreateAssetMenu(fileName = "PlayerColorDataExtended", menuName = "Player/ColorDataExtended")]
public class PlayerColorDataExtended : ScriptableObject
{
[Header("Basic Status")]
public Color displayColor; // Player's visual color
public float maxSpeed; // Maximum running speed
public float runForce; // Acceleration
public float gravityScale; // Gravity scale
public float jumpForce; // Jump force
public string colorName; // Color name
public float chargeCoolTime; // Cooldown time (seconds)
public float defensePower; // Defense power
public float levity; // Repulsion strength when knocked back (lightness)
public float NormalAttackCoolTime; // Normal attack cooldown time (seconds)
[Header("Damage from same-color floors")]
public float hitDamageOnFloor; // Damage amount from the floor
[Header("Color-specific basic action prefabs")]
public GameObject attackEffectPrefab; // Attack effect prefab
}
Benefits
- Parameters for each color can be centrally managed using ScriptableObject, making management easier and reducing mistakes.
- The number of variables in the Player class is significantly reduced, improving code readability and maintainability.
- Adding new colors or adjusting parameters becomes easy, increasing extensibility and consistency.
Specific Code Example: Attack Cooldown
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerAttackCooltimeManager : MonoBehaviour
{
private Dictionary<PlayerColorManager.PlayerColorState, bool> isColorOnCooldown = new();
private Dictionary<PlayerColorManager.PlayerColorState, Coroutine> cooldownCoroutines = new();
[SerializeField] private PlayerColorManager playerColorManager;
private void Awake()
{
// Initialize cooldown for all colors
foreach (PlayerColorManager.PlayerColorState color in System.Enum.GetValues(typeof(PlayerColorManager.PlayerColorState)))
{
isColorOnCooldown[color] = false;
}
}
public bool IsOnCooldown(PlayerColorManager.PlayerColorState color)
{
return isColorOnCooldown[color];
}
public void StartCooldown(PlayerColorManager.PlayerColorState color)
{
float time = playerColorManager.GetDataByColor(color).NormalAttackCoolTime;
if (IsOnCooldown(color))
{
return;
}
cooldownCoroutines[color] = StartCoroutine(CooldownCoroutine(color, time));
}
public void ResetCooldown(PlayerColorManager.PlayerColorState color)
{
if (cooldownCoroutines.ContainsKey(color) && cooldownCoroutines[color] != null)
{
StopCoroutine(cooldownCoroutines[color]);
}
isColorOnCooldown[color] = false; // Reset cooldown when switching colors
}
private IEnumerator CooldownCoroutine(PlayerColorManager.PlayerColorState color, float cooldownTime)
{
isColorOnCooldown[color] = true;
yield return new WaitForSeconds(cooldownTime);
isColorOnCooldown[color] = false;
}
}
Benefits
- Flexible specification that allows immediate cooldown reset when switching colors.
- Cooldown duration for each color can be centrally managed in ScriptableObject.
- Structure that makes it easier to visualize and debug states.
Specific Code Example: Separation of Input and Conditional Branching
// Input collection only (InputManager)
public class InputManager : MonoBehaviour
{
public float Horizontal { get; private set; }
public bool JumpPressed { get; private set; }
public bool ChangeToRed { get; private set; }
public bool NormalAttack { get; private set; }
void Update()
{
Horizontal = (Input.GetKey(KeyCode.D) ? 1f : Input.GetKey(KeyCode.A) ? -1f : 0f);
JumpPressed = Input.GetKeyDown(KeyCode.W);
ChangeToRed = Input.GetKeyDown(KeyCode.Alpha1);
NormalAttack = Input.GetMouseButtonDown(0);
}
}
// Input interpretation and processing dispatch (PlayerInputHandler)
using UnityEngine;
public class PlayerInputHandler : MonoBehaviour
// This class receives input from inputManager and performs conditional branching.
{
[SerializeField] private InputManager inputManager;
[SerializeField] private PlayerMove playerMove;
[SerializeField] private PlayerColorManager playerColorManager;
[SerializeField] private PlayerStateManager playerStateManager;
[SerializeField] private PlayerAttack playerAttack;
[SerializeField] private PlayerAttackCooltimeManager playerAttackCooltimeManager;
void Update()
{
if (playerStateManager.IsTalking) return;
if (inputManager.NormalAttack && !playerAttackCooltimeManager.IsOnCooldown(playerColorManager.GetCurrentState()))
{
playerAttack.normalAttack(playerColorManager.GetCurrentData().attackEffectPrefab);
playerAttackCooltimeManager.StartCooldown(playerColorManager.GetCurrentState());
}
if (!playerColorManager.IsColorChangeCool)
{
if (inputManager.ChangeToRed)
playerColorManager.ChangeColor(PlayerColorManager.PlayerColorState.Red);
else if (inputManager.ChangeToBlue)
playerColorManager.ChangeColor(PlayerColorManager.PlayerColorState.Blue);
else if (inputManager.ChangeToGreen)
playerColorManager.ChangeColor(PlayerColorManager.PlayerColorState.Green);
}
if (inputManager.JumpPress && playerStateManager.IsGround)
{ playerMove.PlayerJump(playerColorManager.GetCurrentData().jumpForce,playerColorManager.GetCurrentData().gravityScale);
playerStateManager.SetJumping();
}
}
void FixedUpdate()
{
if (playerStateManager.IsTalking) return;
float horizontalInput = inputManager.Horizontal;
var data = playerColorManager.GetCurrentData();
if (horizontalInput != 0f && Mathf.Abs(playerMove.CurrentVelocity.x) < data.maxSpeed)
{
playerMove.HandleMove(data.runForce, data.maxSpeed, data.gravityScale, horizontalInput);
}
}
}
Benefits
- By clearly separating input detection and processing, it becomes easier to change input methods or support multiple inputs.
- PlayerInputHandler's responsibility is limited to being a "director of processing," improving maintainability.
- New features can be added via this class, making extension easy.
Specific Code Example: Separation of Color Management
using System;
using UnityEngine;
using UnityEngine.Rendering;
public class PlayerColorManager : MonoBehaviour
{
// Class that holds color data.
// It handles the processing of color data, but does not handle the conditional logic for switching colors (role of PlayerInputHandler) or physical movement (role of PlayerInputHandler & Move).
// UI processing is the responsibility of this class, but only currentData is passed to the UI.
public enum PlayerColorState
{
Red,
Blue,
Green
}
[SerializeField] private PlayerColorDataExtended redData;
[SerializeField] private PlayerColorDataExtended blueData;
[SerializeField] private PlayerColorDataExtended greenData;
[SerializeField] private SpriteRenderer playerSpriteRenderer;
[SerializeField] private ChargeBar chargeBar;
[SerializeField] private PlayerAttackCooltimeManager playerAttackCooltimeManager;
private PlayerColorState currentState;
public bool IsColorChangeCool { get; private set; } = false;
void Start()
{
if (playerSpriteRenderer == null)
playerSpriteRenderer = GetComponent<SpriteRenderer>();
if (playerSpriteRenderer == null)
Debug.LogError("PlayerColorManager: SpriteRenderer is missing");
currentState = PlayerColorState.Red;
GetCurrentData(); // Retrieve initial state data
ApplyColor();
}
public PlayerColorDataExtended GetCurrentData()// Calling this returns the data for the current color
{
return currentState switch
{
PlayerColorState.Red => redData,
PlayerColorState.Blue => blueData,
PlayerColorState.Green => greenData,
_ => throw new ArgumentOutOfRangeException()// Color switching is fundamental to the game, so crash if an exception occurs
};
}
public void ChangeColor(PlayerColorState newState)
{
if (currentState == newState) return;
currentState = newState;
playerAttackCooltimeManager.ResetCooldown(newState);
ApplyColor();
if (chargeBar != null)
{
var data = GetCurrentData();
chargeBar.ChangeCoolTime(data);
IsColorChangeCool = true;
}
}
private void ApplyColor()
{
if (playerSpriteRenderer != null)
{
playerSpriteRenderer.color = GetCurrentData().displayColor;
}
}
public PlayerColorState GetCurrentState()
{
return currentState;
}
public void ResetColorChangeCool()
{
IsColorChangeCool = false;
}
public PlayerColorDataExtended GetDataByColor(PlayerColorState color)
{
return color switch
{
PlayerColorState.Red => redData,
PlayerColorState.Blue => blueData,
PlayerColorState.Green => greenData,
_ => throw new ArgumentOutOfRangeException(nameof(color), color, null)
};
}
}
Reflections and Future Challenges
- Reviewing dependencies: Decoupling using events and delegates.
- Strengthening state transition management: Implementing a state machine.
- High coupling: I want to try using a DI container.
Summary
| Item | Before Refactoring | After Refactoring |
|---|---|---|
| Concentration of Responsibilities | All concentrated in one class | Divided into classes by responsibility |
| State Management Method | Ambiguous with numerous bools | Clarified with enums + state management classes |
| Input Processing | Direct Input calls | Input detection class, bridge class, and processing class |
| Parameter Management | Hard-coded within the class | Data-driven approach using ScriptableObjects |
| Readability/Maintainability | High readability, low maintainability | Low readability, high maintainability |
| Ease of Extension/Modification | Difficult | Easy once accustomed to implementing with SRP in mind |
Conclusion
Through this refactoring, I feel I've learned a bit about how "dividing features into small parts and clarifying roles" directly relates to maintainability and extensibility.
Additionally, being able to actually utilize ScriptableObjects has helped solidify my basic understanding of them.
In the future, I intend to seriously engage with standard approaches such as the SOLID principles and Object-Oriented Design to create games with better design and development in mind.
It has been a long article, but thank you very much for reading to the end.
Supplement: List of classes actually split, added, or redesigned in this refactoring
- InputManager: Responsible only for obtaining input. Flexibility for input changes improved due to separation of responsibilities.
- PlayerInputHandler: Interprets input from InputManager and dispatches it to processes such as movement and attack.
- PlayerMove: Responsible for the physical behavior of movement and jumping. Enhancements like gravityScale initialization were also implemented.
- PlayerColorManager: Manages color states. Controls parameters for each color using ScriptableObject (PlayerColorData).
- PlayerColorData: ScriptableObject that holds parameters such as movement speed and jump force for each color.
- PlayerAttack: Attack processing. Handles effect processing for each color, ensuring reusability.
- PlayerAttackCooltimeManager: Manages attack cooldowns. Collaborates with SO to enable management by color.
- PlayerStateManager: Collectively manages player states such as HP, jumping, and ground detection.
- CheckPlayerstatus: Provides environmental information such as floor and ground detection. Used as criteria for state management.
- PlayerHitDamage: Calculates damage from same-color floors or enemies and executes hit effects.
- PlayerBase: Replaced the previous bloated Playeroperate class and served as the starting point for responsibility separation (deleted during the process as responsibilities bloated, moving to a parallel design excluding Base).
Discussion