Unity Development

My First Unity Game: Lessons from a Rails Developer

After months of building web applications with Rails, I decided to try something completely different: game development with Unity. I thought it would be a world apart from web development, but I was surprised by how many concepts translated between the two.

In this post, I'll share what I learned building my first Unity game and how Rails experience actually helped me understand game development patterns.

Why Unity After Rails?

As a Rails developer, I was comfortable with:
- Object-oriented programming
- Component-based architecture (Rails models, concerns)
- MVC patterns
- Working with frameworks and conventions

Unity appealed to me because:
- Visual development - Drag and drop interface
- C# language - Similar to Ruby in many ways
- Component system - Felt familiar from Rails concerns
- Large community - Lots of learning resources

The Mental Model Shift

Rails: Request/Response Cycle

# User makes request -> Controller processes -> Model queries database -> View renders -> Response
def show
  @user = User.find(params[:id])  # Model
  render :show                    # View
end

Unity: Game Loop

// Every frame: Input -> Update -> Physics -> Render -> Repeat
void Update() {
    HandleInput();     // Check for player input
    UpdateGameState(); // Update object positions, health, etc.
    CheckCollisions(); // Physics and interactions
    // Rendering happens automatically
}

The biggest shift was from event-driven (Rails responds to requests) to frame-driven (Unity runs continuously).

My First Game: A Simple Platformer

I decided to build a 2D platformer where a character can:
- Move left and right
- Jump on platforms
- Collect coins
- Avoid enemies

Simple goals, but they taught me fundamental Unity concepts.

Unity Concepts That Felt Familiar

1. GameObjects ≈ ActiveRecord Models

Rails Model:

class User < ApplicationRecord
  has_many :posts
  has_many :comments

  validates :email, presence: true

  def full_name
    "#{first_name} #{last_name}"
  end
end

Unity GameObject:

// Player GameObject with components
public class Player : MonoBehaviour 
{
    public float speed = 5f;
    public float jumpForce = 400f;

    private Rigidbody2D rb;
    private bool isGrounded;

    void Start() 
    {
        rb = GetComponent<Rigidbody2D>();
    }

    void Update() 
    {
        Move();
        Jump();
    }
}

Similarities:
- Both represent entities in your application
- Both can have properties and behaviors
- Both interact with other objects

Differences:
- GameObjects exist in 3D space
- GameObjects run every frame
- GameObjects have visual representations

2. Components ≈ Rails Concerns

Rails Concern:

module Trackable
  extend ActiveSupport::Concern

  included do
    after_create :track_creation
    after_update :track_update
  end

  def track_activity(action)
    # Track user activity
  end
end

class User < ApplicationRecord
  include Trackable
end

Unity Component:

// Reusable component for any GameObject
public class Health : MonoBehaviour 
{
    public int maxHealth = 100;
    private int currentHealth;

    void Start() 
    {
        currentHealth = maxHealth;
    }

    public void TakeDamage(int damage) 
    {
        currentHealth -= damage;
        if (currentHealth <= 0) {
            Die();
        }
    }

    void Die() 
    {
        Destroy(gameObject);
    }
}

// Attach to any GameObject that needs health
// Player, Enemy, Destructible objects, etc.

Similarities:
- Reusable functionality
- Can be mixed into different classes/objects
- Encapsulate specific behaviors

3. Prefabs ≈ Rails Partials

Rails Partial:

<!-- _user_card.html.erb -->
<div class="user-card">
  <h3><%= user.name %></h3>
  <p><%= user.email %></p>
  <% if user.avatar.present? %>
    <%= image_tag user.avatar %>
  <% end %>
</div>

<!-- Usage -->
<%= render 'user_card', user: @user %>

Unity Prefab:

// Create a Coin prefab with:
// - Sprite Renderer (visual)
// - Collider (for collection)
// - Coin script (behavior)

public class Coin : MonoBehaviour 
{
    public int value = 10;

    void OnTriggerEnter2D(Collider2D other) 
    {
        if (other.CompareTag("Player")) {
            GameManager.Instance.AddScore(value);
            Destroy(gameObject);
        }
    }
}

// Instantiate coins throughout the level
Instantiate(coinPrefab, spawnPosition, Quaternion.identity);

Similarities:
- Reusable templates
- Consistent behavior across instances
- Easy to update all instances

Building the Player Controller

Here's how I built player movement, thinking like a Rails developer:

1. Start with the Interface

public class PlayerController : MonoBehaviour 
{
    [Header("Movement Settings")]
    public float moveSpeed = 5f;
    public float jumpForce = 400f;

    [Header("Ground Detection")]
    public Transform groundCheck;
    public float groundCheckRadius = 0.2f;
    public LayerMask groundLayerMask;

    // Similar to Rails strong parameters - expose what you need
    private Rigidbody2D rb;
    private bool isGrounded;
    private float horizontalInput;
}

2. Separate Concerns

void Update() 
{
    HandleInput();      // Like controller actions
    CheckGrounded();    // Like model validation
    Move();            // Like service objects
    Jump();            // Like service objects
}

void HandleInput() 
{
    horizontalInput = Input.GetAxisRaw("Horizontal");

    if (Input.GetButtonDown("Jump") && isGrounded) {
        Jump();
    }
}

void Move() 
{
    rb.velocity = new Vector2(horizontalInput * moveSpeed, rb.velocity.y);

    // Flip sprite based on direction (like Rails helper methods)
    if (horizontalInput > 0) {
        transform.localScale = new Vector3(1, 1, 1);
    } else if (horizontalInput < 0) {
        transform.localScale = new Vector3(-1, 1, 1);
    }
}

void Jump() 
{
    rb.AddForce(Vector2.up * jumpForce);
}

void CheckGrounded() 
{
    isGrounded = Physics2D.OverlapCircle(groundCheck.position, 
                                       groundCheckRadius, 
                                       groundLayerMask);
}

3. Add Validation and Error Handling

void Start() 
{
    // Like Rails model validations
    rb = GetComponent<Rigidbody2D>();
    if (rb == null) {
        Debug.LogError("PlayerController requires Rigidbody2D component!");
    }

    if (groundCheck == null) {
        Debug.LogError("GroundCheck transform not assigned!");
    }
}

// Like Rails rescue blocks
void OnCollisionEnter2D(Collision2D collision) 
{
    if (collision.gameObject.CompareTag("Enemy")) {
        TakeDamage(10);
    }
}

void TakeDamage(int damage) 
{
    // Like Rails error handling
    try {
        health -= damage;
        if (health <= 0) {
            GameOver();
        }
    } catch (System.Exception e) {
        Debug.LogError($"Error taking damage: {e.Message}");
    }
}

Game Manager Pattern (Like ApplicationController)

Just like Rails has ApplicationController, Unity games often have a GameManager:

public class GameManager : MonoBehaviour 
{
    public static GameManager Instance { get; private set; }

    [Header("Game State")]
    public int score = 0;
    public int lives = 3;
    public bool isGamePaused = false;

    [Header("UI References")]
    public Text scoreText;
    public Text livesText;
    public GameObject gameOverPanel;

    void Awake() 
    {
        // Singleton pattern (like Rails application instance)
        if (Instance != null && Instance != this) {
            Destroy(this);
        } else {
            Instance = this;
            DontDestroyOnLoad(gameObject);
        }
    }

    void Start() 
    {
        UpdateUI();
    }

    // Like controller actions
    public void AddScore(int points) 
    {
        score += points;
        UpdateUI();

        // Like Rails callbacks
        if (score > 1000) {
            UnlockAchievement("High Score");
        }
    }

    public void LoseLife() 
    {
        lives--;
        UpdateUI();

        if (lives <= 0) {
            GameOver();
        }
    }

    void UpdateUI() 
    {
        // Like Rails view updates
        scoreText.text = "Score: " + score;
        livesText.text = "Lives: " + lives;
    }

    void GameOver() 
    {
        isGamePaused = true;
        gameOverPanel.SetActive(true);
        Time.timeScale = 0f; // Pause game
    }
}

Collectibles System (Like Rails Resources)

Creating a coin collection system felt like building a Rails resource:

// Like a Rails model
[System.Serializable]
public class Collectible 
{
    public string name;
    public int value;
    public Sprite sprite;
    public AudioClip collectSound;
}

// Like a Rails controller
public class CollectibleController : MonoBehaviour 
{
    public Collectible collectibleData;

    void Start() 
    {
        // Like Rails view rendering
        GetComponent<SpriteRenderer>().sprite = collectibleData.sprite;
    }

    void OnTriggerEnter2D(Collider2D other) 
    {
        if (other.CompareTag("Player")) {
            Collect(other.gameObject);
        }
    }

    void Collect(GameObject player) 
    {
        // Like Rails controller action
        GameManager.Instance.AddScore(collectibleData.value);

        // Like Rails after_action callback
        PlayCollectEffect();

        // Like Rails destroy action
        Destroy(gameObject);
    }

    void PlayCollectEffect() 
    {
        // Like Rails flash messages
        if (collectibleData.collectSound != null) {
            AudioSource.PlayClipAtPoint(collectibleData.collectSound, transform.position);
        }
    }
}

Enemy AI (Like Rails Services)

Creating enemy behavior felt like writing Rails service objects:

public class EnemyAI : MonoBehaviour 
{
    [Header("AI Settings")]
    public float moveSpeed = 2f;
    public float detectionRange = 5f;
    public float attackRange = 1.5f;

    private Transform player;
    private Rigidbody2D rb;
    private enum EnemyState { Patrol, Chase, Attack }
    private EnemyState currentState = EnemyState.Patrol;

    void Start() 
    {
        rb = GetComponent<Rigidbody2D>();
        player = GameObject.FindGameObjectWithTag("Player").transform;
    }

    void Update() 
    {
        // Like Rails service object with different actions
        switch (currentState) {
            case EnemyState.Patrol:
                Patrol();
                break;
            case EnemyState.Chase:
                ChasePlayer();
                break;
            case EnemyState.Attack:
                AttackPlayer();
                break;
        }

        CheckPlayerDistance();
    }

    void Patrol() 
    {
        // Basic back-and-forth movement
        rb.velocity = new Vector2(moveSpeed, rb.velocity.y);
    }

    void ChasePlayer() 
    {
        Vector2 direction = (player.position - transform.position).normalized;
        rb.velocity = new Vector2(direction.x * moveSpeed, rb.velocity.y);
    }

    void AttackPlayer() 
    {
        // Attack logic here
        Debug.Log("Attacking player!");
    }

    void CheckPlayerDistance() 
    {
        float distanceToPlayer = Vector2.Distance(transform.position, player.position);

        // Like Rails conditional logic
        if (distanceToPlayer <= attackRange) {
            currentState = EnemyState.Attack;
        } else if (distanceToPlayer <= detectionRange) {
            currentState = EnemyState.Chase;
        } else {
            currentState = EnemyState.Patrol;
        }
    }
}

Scene Management (Like Rails Routing)

Unity's scene system felt similar to Rails routing:

public class SceneManager : MonoBehaviour 
{
    // Like Rails routes
    public void LoadMainMenu() 
    {
        UnityEngine.SceneManagement.SceneManager.LoadScene("MainMenu");
    }

    public void LoadLevel(int levelNumber) 
    {
        UnityEngine.SceneManagement.SceneManager.LoadScene($"Level{levelNumber}");
    }

    public void RestartCurrentLevel() 
    {
        Scene currentScene = UnityEngine.SceneManagement.SceneManager.GetActiveScene();
        UnityEngine.SceneManagement.SceneManager.LoadScene(currentScene.name);
    }

    // Like Rails redirect_to with parameters
    public void LoadLevelWithScore(int levelNumber, int currentScore) 
    {
        PlayerPrefs.SetInt("CarryOverScore", currentScore);
        LoadLevel(levelNumber);
    }
}

What Surprised Me

1. Performance Matters More

In Rails, I could be somewhat careless with database queries and fix them later. In Unity, every frame counts:

// Bad - creates garbage every frame
void Update() 
{
    Vector3 playerPosition = new Vector3(player.x, player.y, 0f);
}

// Good - reuse variables
private Vector3 playerPosition;

void Update() 
{
    playerPosition.Set(player.x, player.y, 0f);
}

2. Visual Debugging is Amazing

Unity's inspector lets you see and modify values in real-time while the game runs. It's like having rails console but visual and live.

3. Component Composition is Powerful

Instead of inheritance (like Rails STI), Unity favors composition. You build complex behaviors by combining simple components.

4. State Management is Critical

Games have much more complex state than web apps. You need to track player position, health, inventory, level progress, etc., all updating every frame.

Common Mistakes I Made

1. Overusing Update()

// Bad - checking every frame
void Update() 
{
    if (Input.GetKeyDown(KeyCode.Space)) {
        Jump();
    }
}

// Better - use events or coroutines for less frequent checks

2. Not Using Object Pooling

// Bad - creates/destroys objects constantly
Instantiate(bulletPrefab, firePosition, Quaternion.identity);

// Better - reuse objects from a pool
BulletPool.Instance.GetBullet();

3. Tight Coupling

// Bad - direct references everywhere
public class Enemy : MonoBehaviour 
{
    public Player player;
    public GameManager gameManager;
    public UIManager uiManager;
}

// Better - use events or singletons
public class Enemy : MonoBehaviour 
{
    void Die() 
    {
        EventManager.TriggerEvent("EnemyDied", this);
    }
}

Key Takeaways

What Transferred Well from Rails:
- Object-oriented thinking
- Separation of concerns
- Component-based architecture
- Debugging and testing mindset
- Reading documentation and community resources

What Was Different:
- Performance optimization mindset
- Frame-based instead of request-based thinking
- Visual and spatial reasoning
- Real-time system constraints
- Much more complex state management

Skills I Developed:
- Understanding game loops and frame rates
- Working with physics and collisions
- Managing complex object hierarchies
- Optimizing for performance
- Visual design and user experience

Next Steps in Game Development

After completing my first platformer, I want to explore:
- Multiplayer networking (feels like Rails API development)
- Save/load systems (like Rails database persistence)
- Procedural generation (like Rails factories for content)
- Mobile deployment (like Rails deployment to Heroku)

For Rails Developers Considering Unity

You'll feel comfortable with:
- C# syntax (similar to Ruby)
- Component patterns
- Documentation and community
- Problem-solving approaches

You'll need to learn:
- Game-specific concepts (physics, rendering, audio)
- Performance optimization techniques
- Visual design principles
- Real-time programming patterns

Start with:
- Simple 2D games
- Unity's official tutorials
- Small, achievable projects
- Prototype first, polish later

Conclusion

Building my first Unity game as a Rails developer was challenging but rewarding. Many fundamental programming concepts transferred well, but the real-time, visual nature of games required new ways of thinking.

The biggest lesson: good software design principles apply everywhere. Whether you're building a web app or a game, clean code

Christopher Lim

Christopher Lim

Rails developer and Unity explorer. Family man, lifelong learner, and builder turning ideas into polished applications. Passionate about quality software development and continuous improvement.

Back to All Posts
Reading time: 10 min read

Enjoyed this unity development post?

Follow me for more insights on unity development, Rails development, and software engineering excellence.