From: Game Development Patterns with Unity 2021 (2nd Edition)
By David Baron
⚠️ Important Note: This is a crash course - we're covering advanced topics quickly.
The goal is awareness and familiarity, not complete mastery. If you need deeper understanding,
refer to Baron's book (available at Dulaney Browne Library) and learn.unity.com resources.
Today's Learning Objectives
C# Advanced Features
Static members
Events & Delegates
Generics
Serialization
Unity Engine Features
Prefabs
Unity Events
ScriptableObjects
Coroutines
For Advanced Students: If you're already comfortable with these concepts,
use this time to help your peers or explore Baron's book for deeper examples.
Quick Review: What You Should Know
C# Fundamentals
Access modifiers (public, private)
Primitive types (int, string, bool, float)
Arrays and collections
Inheritance (base/derived classes)
Unity Basics
MonoBehaviour scripts
Creating/manipulating GameObjects
Awake(), Start(), Update()
Component-based architecture
Not comfortable with these? Please see me after class or during office hours.
Check out Unity's "Junior Programmer" pathway at learn.unity.com for a refresher.
C# Advanced Feature: Static
What is Static?
Methods and members with the static keyword can be accessed directly
by class name without creating an instance.
Why Use Static?
✓ Globally accessible from anywhere
✓ Shared across all instances
✓ Perfect for managers and utilities
✓ No need to find or reference objects
Static Example: Event Bus
using UnityEngine.Events;
using System.Collections.Generic;
namespace Chapter.EventBus
{
public classRaceEventBus
{
// Static dictionary - shared globallyprivate static readonly
IDictionary<RaceEventType, UnityEvent> Events =
new Dictionary<RaceEventType, UnityEvent>();
// Static method - call without instancepublic static voidSubscribe(
RaceEventType eventType, UnityAction listener)
{
UnityEvent thisEvent;
if (Events.TryGetValue(eventType, out thisEvent))
{
thisEvent.AddListener(listener);
}
else
{
thisEvent = new UnityEvent();
thisEvent.AddListener(listener);
Events.Add(eventType, thisEvent);
}
}
}
}
Using Static Members
// Call static method directly - no instance needed!
RaceEventBus.Subscribe(RaceEventType.START, OnRaceStart);
// vs. Non-static (would need instance)
RaceEventBus bus = new RaceEventBus();
bus.Subscribe(RaceEventType.START, OnRaceStart);
✅ Good Use Cases
Game managers
Utility functions
Global event systems
Configuration data
❌ Avoid For
Instance-specific data
Multiple object types
Testability concerns
Everything (don't overuse!)
C# Advanced Feature: Events
What are Events?
Events allow a publisher object to send signals that
subscriber objects can receive. Perfect for decoupled,
event-driven architectures.
Publisher
Declares and invokes the event
Subscriber
Listens for and responds to events
Why Events? Decoupling! Publishers don't need to know who's listening.
Events Example: Publisher
using UnityEngine;
using System.Collections;
public classCountdownTimer : MonoBehaviour
{
// Declare delegate typespublic delegate void TimerStarted();
public delegate void TimerEnded();
// Declare events using those delegatespublic static event TimerStarted OnTimerStarted;
public static event TimerEnded OnTimerEnded;
[SerializeField]
private float duration = 5.0f;
voidStart()
{
StartCoroutine(StartCountdown());
}
private IEnumerator StartCountdown()
{
// Invoke event (notify subscribers)if (OnTimerStarted != null)
OnTimerStarted();
while (duration > 0)
{
yield return new WaitForSeconds(1f);
duration--;
}
if (OnTimerEnded != null)
OnTimerEnded();
}
}
C# Advanced Feature: Delegates
What are Delegates?
Delegates hold references to functions. They're function pointers
that store memory addresses of methods. Think of them as an address book of function locations.
Key Concepts
Function pointers: Store memory addresses to methods
Multicast: Can hold multiple function references
Invoke all: Call all subscribed functions at once
Type-safe: Enforces matching signatures
Delegates Example: Subscriber
using UnityEngine;
public classBuzzer : MonoBehaviour
{
voidOnEnable()
{
// Subscribe: Assign local functions to delegates
CountdownTimer.OnTimerStarted += PlayStartBuzzer;
CountdownTimer.OnTimerEnded += PlayEndBuzzer;
}
voidOnDisable()
{
// Unsubscribe: Always clean up!
CountdownTimer.OnTimerStarted -= PlayStartBuzzer;
CountdownTimer.OnTimerEnded -= PlayEndBuzzer;
}
voidPlayStartBuzzer()
{
Debug.Log("[BUZZER] : Play start buzzer!");
}
voidPlayEndBuzzer()
{
Debug.Log("[BUZZER] : Play end buzzer!");
}
}
Critical: Always unsubscribe in OnDisable() to prevent memory leaks!
Events + Delegates: How They Work Together
The Relationship
Delegate = Type definition (what signature functions must have) Event = Special delegate with restricted access Together = Safe, decoupled communication system
I'll be demonstrating with Rider, but VS Code works great too!
Setting Your Editor in Unity
Edit → Preferences (Windows) or Unity → Settings (Mac)
External Tools → External Script Editor
Select your preferred editor (Rider or VS Code)
Click "Regenerate project files" if needed
Opening Scripts
Double-click any .cs file in Unity
Your configured editor launches automatically
Right-click → Open C# Project opens entire solution
Why Rider?
✓ Free student license (apply with your OCU email)
✓ Excellent Unity integration and autocomplete
✓ Advanced refactoring tools
✓ Built-in debugger for Unity
VS Code is also excellent - use what works best for you!
Practice: Events & Delegates 💻
15-Minute Challenge: Player Damage System
Setup Instructions (First Time)
Create C# Script in Unity:
Right-click in Project window → Create → C# Script
Name it Player (Unity will add .cs automatically)
Open in Rider:
Double-click the Player.cs file in Unity
Rider will launch and open the script
Note: VS Code users - same process, double-click opens your configured editor
Repeat for UIHealthBar: Create another script named UIHealthBar
Challenge Tasks:
In Player.cs: Add a health variable (int or float)
Declare a delegate type and event for OnPlayerDamaged
Create a TakeDamage() method that reduces health and invokes the event
In UIHealthBar.cs: Subscribe to the event in OnEnable()
Unsubscribe in OnDisable() (always clean up!)
Log the new health value when the event fires
Bonus: Add an OnPlayerDied event when health reaches zero! Rider Tip: Use Alt+Enter for quick fixes and code generation!
C# Advanced Feature: Generics
What are Generics?
Generics permit deferred type specification until the class is
instantiated. Create type-safe, reusable code templates.
// Generic Singleton - works with ANY component type!public class Singleton<T> : MonoBehaviour
where T : Component
{
private static T _instance;
public static T Instance
{
get
{
if (_instance == null)
{
_instance = FindObjectOfType<T>();
}
return _instance;
}
}
}
Using Generics
// Use the generic Singleton for different managerspublic classGameManager : Singleton<GameManager>
{
// GameManager-specific code
}
public classAudioManager : Singleton<AudioManager>
{
// AudioManager-specific code
}
// Access them anywhere
GameManager.Instance.StartGame();
AudioManager.Instance.PlaySound();
Benefits of Generics
✓ Write once, use for many types
✓ Type safety at compile time
✓ No type casting needed
✓ Cleaner, more maintainable code
C# Advanced Feature: Serialization
What is Serialization?
Converting an object instance into binary or text format. Allows you to
save and load object state to/from files.
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
private voidSerializePlayerData(PlayerData playerData)
{
// Create binary formatter
BinaryFormatter bf = new BinaryFormatter();
// Create file at persistent data path
FileStream file = File.Create(
Application.persistentDataPath + "/playerData.dat");
// Serialize object to file
bf.Serialize(file, playerData);
file.Close();
}
Use Cases: Save games, player preferences, game state persistence
Unity Engine Feature: Prefabs
What are Prefabs?
Prefabricated containers of assembled GameObjects and components.
Reusable building blocks for your game entities.
Why Prefabs?
Reusability across scenes
Consistent object templates
Easy mass updates
Runtime instantiation
Examples
Enemy types
Pickup items
Vehicle variants
UI elements
In Blade Racer: Each motorcycle type, obstacle, and pickup will be a prefab!
Unity Engine Feature: Unity Events
What are Unity Events?
Unity's native event system - similar to C# events but with
Inspector integration for visual configuration.
Unity Events
Visible in Inspector
Designer-friendly
No code required
Drag-and-drop wiring
C# Events
Code-only
Programmer-focused
Type-safe delegates
Better performance
Best Practice: Use Unity Events for designer-exposed functionality,
C# events for internal systems.
Unity Engine Feature: ScriptableObjects
What are ScriptableObjects?
Data containers that live as assets. Separate data from behavior.
Use MonoBehaviour for logic, ScriptableObject for data.
using UnityEngine;
[CreateAssetMenu(fileName = "NewSword",
menuName = "Weaponry/Sword")]
public classSword : ScriptableObject
{
public string swordName;
public string swordPrefab;
public int damage;
public float attackSpeed;
}
Power: Non-programmers can create new sword types through the Unity Editor
without touching code!
ScriptableObjects in Action
Workflow Benefits
Right-click in Project → Create → Weaponry → Sword
Name it "FireSword"
Fill in values in Inspector
Reference it in your code or prefabs
Data is now reusable across scenes!
Perfect For
Weapon stats
Enemy configurations
Level data
Game settings
Benefits
Designer authoring
Memory efficient
Version control friendly
Easy balancing
Unity Engine Feature: Coroutines
What are Coroutines?
Functions with the ability to wait, pause, and time their execution.
Provide concurrency (not parallelism).
Normal Function vs Coroutine
Normal Function
Executes start to finish in one frame
Coroutine
Can pause and resume across frames
Coroutines Example
using UnityEngine;
using System.Collections;
public classCountdownTimer : MonoBehaviour
{
private float _duration = 10.0f;
// IEnumerator return type = coroutine
IEnumerator Start()
{
Debug.Log("Timer Started!");
yield return StartCoroutine(WaitAndPrint(1.0f));
Debug.Log("Timer Ended!");
}
IEnumerator WaitAndPrint(float waitTime)
{
while (Time.time < _duration)
{
// Wait 1 second between iterationsyield return new WaitForSeconds(waitTime);
Debug.Log("Seconds: " + Mathf.Round(Time.time));
}
}
}
Key:yield return pauses execution and resumes next frame (or after wait)
When to Use Coroutines
✅ Perfect For
Timers and countdowns
Smooth animations
Delayed actions
Sequences over time
Loading operations
❌ Avoid For
Every-frame updates
Heavy computations
Instant results needed
True parallelism
// Common coroutine yields:yield return null; // Wait one frameyield return new WaitForSeconds(2f); // Wait 2 secondsyield return new WaitForEndOfFrame(); // Wait until frame endyield return new WaitUntil(() => condition); // Wait for condition
Practice: Coroutines 💻
20-Minute Challenge: Power-Up System
Setup Instructions
Create PowerUp Script:
In Unity: Right-click Project → Create → C# Script
Name: PowerUp
Double-click to open in Rider
Create GameObject:
Hierarchy → Right-click → Create Empty
Name: "PowerUpManager"
Drag PowerUp.cs onto PowerUpManager
Add Test Trigger:
In Update(): Check for Input.GetKeyDown(KeyCode.Space)
Use yield return new WaitForSeconds(5f) to wait 5 seconds
Log "Power-Up Ended!" after wait
Call it with StartCoroutine(PowerUpDuration()) when spacebar pressed
Bonus: Add countdown (4, 3, 2, 1...) using a loop with 1-second waits
Rider Tips: Rider auto-completes Unity API methods like StartCoroutine! Testing: Press Play in Unity, then spacebar to trigger the power-up and watch the Console!
Prefab: TurboPickup with collider and visual effect
Event: OnTurboActivated (notify UI to show boost meter)
Coroutine: Temporary speed boost over time
Static: GameManager.Instance to track boost status
Generics: Singleton<GameManager> for global access
See the power? Each feature solves a specific problem. Combined,
they create elegant, maintainable systems!
Important Disclaimer
⚠️ About the Code Examples
All code shown today (and throughout this course) is for educational purposes only.
The examples have not been optimized and are not meant for production use.
What This Means
Focus is on learning concepts, not performance
Production code requires error handling, optimization, edge cases
These are simplified examples to illustrate patterns