Skip to main content

Appendix B - Best Design Practices For Unity Game Developers

Introduction

Game design is the art and science of creating engaging, balanced, and enjoyable player experiences. While programming skills are essential for implementing game mechanics, understanding design principles is equally important for creating successful games. This guide covers best practices for game design specifically in the context of Unity development.


Core Game Design Principles

Game Feel and Player Experience

  1. Responsive Controls:

    • Implement tight, responsive controls with minimal input lag.
    • Add subtle visual and audio feedback for every player action.
    • Use animation curves to create natural movement acceleration and deceleration.
    // Example of responsive player movement with acceleration
    public class PlayerMovement : MonoBehaviour
    {
    [SerializeField] private float maxSpeed = 10f;
    [SerializeField] private float accelerationTime = 0.3f;
    [SerializeField] private float decelerationTime = 0.2f;

    private float currentSpeed;
    private float velocityXSmoothing;

    private void Update()
    {
    float targetVelocityX = Input.GetAxis("Horizontal") * maxSpeed;

    // Use SmoothDamp for natural acceleration/deceleration
    float smoothTime = (Mathf.Abs(targetVelocityX) > 0.01f) ? accelerationTime : decelerationTime;
    currentSpeed = Mathf.SmoothDamp(currentSpeed, targetVelocityX, ref velocityXSmoothing, smoothTime);

    transform.Translate(new Vector3(currentSpeed * Time.deltaTime, 0, 0));
    }
    }
  2. Game Feel Enhancements:

    • Add screen shake for impactful moments.
    • Implement "juice" elements like particle effects, sound effects, and animation squash/stretch.
    • Use time manipulation (slow-motion, freeze frames) for dramatic moments.
    public class CameraShake : MonoBehaviour
    {
    public IEnumerator Shake(float duration, float magnitude)
    {
    Vector3 originalPosition = transform.localPosition;
    float elapsed = 0f;

    while (elapsed < duration)
    {
    float x = Random.Range(-1f, 1f) * magnitude;
    float y = Random.Range(-1f, 1f) * magnitude;

    transform.localPosition = new Vector3(x, y, originalPosition.z);

    elapsed += Time.deltaTime;
    yield return null;
    }

    transform.localPosition = originalPosition;
    }
    }

Balancing and Progression

  1. Difficulty Curves:

    • Design a smooth difficulty progression that challenges players without frustrating them.
    • Implement dynamic difficulty adjustment based on player performance.
    • Use data-driven balancing with ScriptableObjects for easy tuning.
    [CreateAssetMenu(fileName = "DifficultySettings", menuName = "Game/Difficulty Settings")]
    public class DifficultySettings : ScriptableObject
    {
    [Header("Enemy Spawning")]
    public AnimationCurve enemySpawnRateCurve;
    public AnimationCurve enemyHealthMultiplierCurve;
    public AnimationCurve enemyDamageMultiplierCurve;

    [Header("Player Progression")]
    public AnimationCurve experienceRequirementCurve;
    public AnimationCurve playerDamageMultiplierCurve;

    public float GetEnemySpawnRate(float gameProgressPercent)
    {
    return enemySpawnRateCurve.Evaluate(gameProgressPercent);
    }

    public float GetEnemyHealthMultiplier(float gameProgressPercent)
    {
    return enemyHealthMultiplierCurve.Evaluate(gameProgressPercent);
    }

    // Additional methods for other parameters
    }
  2. Reward Systems:

    • Design meaningful rewards that match player effort.
    • Create multiple progression systems (character levels, gear upgrades, skill trees).
    • Balance risk vs. reward to encourage strategic play.

Player Guidance and Learning

  1. Tutorial Design:

    • Integrate tutorials naturally into gameplay rather than front-loading instructions.
    • Teach mechanics through guided discovery and contextual hints.
    • Use progressive disclosure to introduce complexity gradually.
    public class TutorialManager : MonoBehaviour
    {
    [System.Serializable]
    public class TutorialStep
    {
    public string triggerCondition;
    public GameObject tutorialPrompt;
    public bool requiresCompletion;
    public string completionCondition;
    }

    [SerializeField] private TutorialStep[] tutorialSteps;
    private int currentStepIndex = -1;

    private void Update()
    {
    if (currentStepIndex < tutorialSteps.Length - 1)
    {
    CheckForNextTutorialTrigger();
    }

    if (currentStepIndex >= 0 && tutorialSteps[currentStepIndex].requiresCompletion)
    {
    CheckForStepCompletion();
    }
    }

    private void CheckForNextTutorialTrigger()
    {
    // Implementation to check if next tutorial step should trigger
    }

    private void CheckForStepCompletion()
    {
    // Implementation to check if current step is completed
    }
    }
  2. Feedback Systems:

    • Provide clear feedback for player actions and game state changes.
    • Use visual, audio, and haptic feedback to reinforce important information.
    • Implement progressive hint systems for players who get stuck.

Level Design Best Practices

Spatial Design and Flow

  1. Level Flow and Pacing:

    • Create clear paths with recognizable landmarks for navigation.
    • Design levels with alternating intensity (combat areas, exploration, puzzle sections).
    • Use level geometry to naturally guide players toward objectives.
  2. Environmental Storytelling:

    • Tell stories through the environment rather than exposition.
    • Use props, lighting, and architecture to convey narrative information.
    • Create visually distinct areas that communicate their purpose.
    public class EnvironmentalStoryTrigger : MonoBehaviour
    {
    [SerializeField] private string storyElementID;
    [SerializeField] private GameObject visualEffect;
    [SerializeField] private AudioClip ambientSound;
    [SerializeField] private string playerThought;

    private bool hasTriggered = false;

    private void OnTriggerEnter(Collider other)
    {
    if (!hasTriggered && other.CompareTag("Player"))
    {
    hasTriggered = true;

    // Trigger environmental storytelling elements
    if (visualEffect != null)
    visualEffect.SetActive(true);

    if (ambientSound != null)
    AudioSource.PlayClipAtPoint(ambientSound, transform.position);

    if (!string.IsNullOrEmpty(playerThought))
    GameManager.Instance.ShowPlayerThought(playerThought);

    // Record that player discovered this story element
    StoryManager.Instance.DiscoverStoryElement(storyElementID);
    }
    }
    }

Level Optimization

  1. Occlusion Culling and LOD:

    • Set up proper occlusion culling to avoid rendering unseen objects.
    • Implement Level of Detail (LOD) for complex models.
    • Use object pooling for frequently spawned objects.
    public class LODManager : MonoBehaviour
    {
    [System.Serializable]
    public class LODLevel
    {
    public float distanceThreshold;
    public GameObject highDetailModel;
    public GameObject mediumDetailModel;
    public GameObject lowDetailModel;
    }

    [SerializeField] private LODLevel[] lodLevels;
    [SerializeField] private Transform playerTransform;

    private void Update()
    {
    foreach (var lod in lodLevels)
    {
    float distanceToPlayer = Vector3.Distance(transform.position, playerTransform.position);

    if (distanceToPlayer <= lod.distanceThreshold)
    {
    // Show high detail
    lod.highDetailModel.SetActive(true);
    lod.mediumDetailModel.SetActive(false);
    lod.lowDetailModel.SetActive(false);
    }
    else if (distanceToPlayer <= lod.distanceThreshold * 2)
    {
    // Show medium detail
    lod.highDetailModel.SetActive(false);
    lod.mediumDetailModel.SetActive(true);
    lod.lowDetailModel.SetActive(false);
    }
    else
    {
    // Show low detail
    lod.highDetailModel.SetActive(false);
    lod.mediumDetailModel.SetActive(false);
    lod.lowDetailModel.SetActive(true);
    }
    }
    }
    }
  2. Scene Management:

    • Break large levels into smaller scenes that can be loaded additively.
    • Implement streaming systems for open-world games.
    • Use asset bundles for downloadable content and updates.

UI/UX Design for Games

Game UI Best Practices

  1. Intuitive Interface Design:

    • Design UI that communicates information clearly without overwhelming the player.
    • Use consistent visual language throughout your game.
    • Implement contextual UI that appears only when relevant.
    public class ContextualUIManager : MonoBehaviour
    {
    [SerializeField] private GameObject interactionPrompt;
    [SerializeField] private GameObject combatUI;
    [SerializeField] private GameObject inventoryUI;

    private void Update()
    {
    // Show interaction prompt only when near interactable objects
    bool nearInteractable = Physics.OverlapSphere(transform.position, interactionRadius, interactableLayer).Length > 0;
    interactionPrompt.SetActive(nearInteractable);

    // Show combat UI only during combat
    combatUI.SetActive(GameManager.Instance.InCombat);

    // Other contextual UI logic
    }

    public void ToggleInventory()
    {
    inventoryUI.SetActive(!inventoryUI.activeSelf);
    }
    }
  2. Accessibility Features:

    • Implement colorblind modes and high-contrast options.
    • Add text scaling and readable fonts.
    • Support remappable controls and alternative input methods.
    • Include options to reduce screen shake, flashing effects, and motion.
    [System.Serializable]
    public class AccessibilitySettings
    {
    public bool colorblindMode;
    public ColorblindType colorblindType;
    public bool highContrastMode;
    public float textScale = 1.0f;
    public bool reduceMotion;
    public bool reduceFlashing;
    public float screenShakeIntensity = 1.0f;
    }

    public enum ColorblindType
    {
    None,
    Protanopia,
    Deuteranopia,
    Tritanopia
    }

    public class AccessibilityManager : MonoBehaviour
    {
    public static AccessibilityManager Instance;

    [SerializeField] private AccessibilitySettings settings;

    // Implementation of accessibility features
    }

Player Feedback Systems

  1. Visual Feedback:

    • Use animation, particles, and effects to communicate game state.
    • Implement clear visual indicators for health, damage, and status effects.
    • Design intuitive HUD elements that don't obstruct gameplay.
  2. Audio Design:

    • Create distinctive sound effects for different actions and events.
    • Implement spatial audio for immersion and gameplay information.
    • Use adaptive music that responds to game state and player actions.
    public class AdaptiveMusicSystem : MonoBehaviour
    {
    [SerializeField] private AudioClip explorationTheme;
    [SerializeField] private AudioClip combatTheme;
    [SerializeField] private AudioClip bossTheme;
    [SerializeField] private AudioClip victoryTheme;

    [SerializeField] private float crossfadeDuration = 2.0f;

    private AudioSource primarySource;
    private AudioSource secondarySource;
    private bool isPrimaryPlaying = true;

    private void Start()
    {
    // Setup audio sources
    primarySource = gameObject.AddComponent<AudioSource>();
    secondarySource = gameObject.AddComponent<AudioSource>();

    // Start with exploration theme
    primarySource.clip = explorationTheme;
    primarySource.loop = true;
    primarySource.Play();
    }

    public void TransitionToMusic(AudioClip newTheme)
    {
    StartCoroutine(CrossfadeMusic(newTheme));
    }

    private IEnumerator CrossfadeMusic(AudioClip newTheme)
    {
    AudioSource fadeOutSource = isPrimaryPlaying ? primarySource : secondarySource;
    AudioSource fadeInSource = isPrimaryPlaying ? secondarySource : primarySource;

    // Setup fade-in source
    fadeInSource.clip = newTheme;
    fadeInSource.volume = 0;
    fadeInSource.Play();

    // Crossfade
    float timer = 0;
    while (timer < crossfadeDuration)
    {
    timer += Time.deltaTime;
    float t = timer / crossfadeDuration;

    fadeOutSource.volume = Mathf.Lerp(1, 0, t);
    fadeInSource.volume = Mathf.Lerp(0, 1, t);

    yield return null;
    }

    // Cleanup
    fadeOutSource.Stop();
    isPrimaryPlaying = !isPrimaryPlaying;
    }
    }

Game Systems Design

Economy and Progression Systems

  1. In-Game Economy Design:

    • Balance sources and sinks of resources to create a stable economy.
    • Design multiple currencies with distinct purposes.
    • Create meaningful choices for resource allocation.
    [CreateAssetMenu(fileName = "EconomySettings", menuName = "Game/Economy Settings")]
    public class EconomySettings : ScriptableObject
    {
    [System.Serializable]
    public class CurrencySettings
    {
    public string currencyName;
    public Sprite currencyIcon;
    public float baseEarnRate;
    public float maxStorageBase;
    public AnimationCurve earnRateCurve;
    public AnimationCurve storageCurve;
    }

    [SerializeField] private CurrencySettings[] currencies;

    [System.Serializable]
    public class ResourceConversion
    {
    public string fromCurrency;
    public string toCurrency;
    public float conversionRate;
    public float conversionCost;
    }

    [SerializeField] private ResourceConversion[] conversions;

    // Methods for calculating currency earn rates, storage limits, etc.
    }
  2. Progression Systems:

    • Design interlocking progression systems (character level, gear, skills).
    • Create meaningful milestones that unlock new gameplay options.
    • Balance vertical progression (power increase) with horizontal progression (new options).

Combat and AI Systems

  1. Combat Design:

    • Create clear feedback for hits, damage, and status effects.
    • Balance offensive and defensive options.
    • Design encounters with varied enemy types and behaviors.
  2. AI Behavior Design:

    • Implement state machines or behavior trees for complex AI.
    • Create AI that appears intelligent but remains fair and readable.
    • Design AI that complements your game's core mechanics.
    public class EnemyAI : MonoBehaviour
    {
    [SerializeField] private float sightRange = 10f;
    [SerializeField] private float attackRange = 2f;
    [SerializeField] private float patrolSpeed = 2f;
    [SerializeField] private float chaseSpeed = 4f;
    [SerializeField] private Transform[] patrolPoints;

    private enum State { Patrol, Chase, Attack, Retreat, Stunned }
    private State currentState;

    private Transform player;
    private int currentPatrolIndex = 0;
    private NavMeshAgent agent;

    private void Awake()
    {
    agent = GetComponent<NavMeshAgent>();
    player = GameObject.FindGameObjectWithTag("Player").transform;
    currentState = State.Patrol;
    }

    private void Update()
    {
    switch (currentState)
    {
    case State.Patrol:
    Patrol();
    break;
    case State.Chase:
    ChasePlayer();
    break;
    case State.Attack:
    AttackPlayer();
    break;
    case State.Retreat:
    Retreat();
    break;
    case State.Stunned:
    // Do nothing while stunned
    break;
    }

    // Check for state transitions
    UpdateState();
    }

    private void UpdateState()
    {
    float distanceToPlayer = Vector3.Distance(transform.position, player.position);

    // State transition logic
    switch (currentState)
    {
    case State.Patrol:
    if (distanceToPlayer <= sightRange)
    SetState(State.Chase);
    break;

    case State.Chase:
    if (distanceToPlayer <= attackRange)
    SetState(State.Attack);
    else if (distanceToPlayer > sightRange * 1.5f)
    SetState(State.Patrol);
    break;

    case State.Attack:
    if (distanceToPlayer > attackRange)
    SetState(State.Chase);
    break;

    // Additional state transitions
    }
    }

    private void SetState(State newState)
    {
    // Exit actions for current state
    switch (currentState)
    {
    case State.Attack:
    // Cancel attack animations, etc.
    break;
    }

    currentState = newState;

    // Enter actions for new state
    switch (newState)
    {
    case State.Patrol:
    agent.speed = patrolSpeed;
    break;

    case State.Chase:
    agent.speed = chaseSpeed;
    break;

    // Setup for other states
    }
    }

    // Implementation of state behaviors (Patrol, ChasePlayer, etc.)
    }

Performance and Optimization

Technical Optimization

  1. Frame Rate Optimization:

    • Target consistent frame rates rather than maximum performance.
    • Implement frame rate limiting and adaptive quality settings.
    • Profile your game to identify and address performance bottlenecks.
  2. Mobile and Cross-Platform Considerations:

    • Design UI that works across different screen sizes and aspect ratios.
    • Implement touch controls that feel natural on mobile devices.
    • Create scalable graphics settings for different hardware capabilities.
    public class AdaptiveQualityManager : MonoBehaviour
    {
    [SerializeField] private int targetFrameRate = 60;
    [SerializeField] private float frameRateCheckInterval = 1.0f;
    [SerializeField] private int frameRateThreshold = 5;

    [SerializeField] private QualitySettings[] qualityLevels;

    private int currentQualityIndex;
    private float frameRateCheckTimer;
    private int frameCount;

    [System.Serializable]
    public class QualitySettings
    {
    public string settingName;
    public int renderDistance;
    public bool enablePostProcessing;
    public int shadowQuality;
    public int textureQuality;
    public bool enableParticles;
    }

    private void Start()
    {
    // Start with medium quality
    currentQualityIndex = qualityLevels.Length / 2;
    ApplyQualitySettings();
    }

    private void Update()
    {
    frameCount++;
    frameRateCheckTimer += Time.unscaledDeltaTime;

    if (frameRateCheckTimer >= frameRateCheckInterval)
    {
    float currentFrameRate = frameCount / frameRateCheckTimer;
    frameCount = 0;
    frameRateCheckTimer = 0;

    // Adjust quality based on frame rate
    if (currentFrameRate < targetFrameRate - frameRateThreshold && currentQualityIndex > 0)
    {
    currentQualityIndex--;
    ApplyQualitySettings();
    }
    else if (currentFrameRate > targetFrameRate + frameRateThreshold && currentQualityIndex < qualityLevels.Length - 1)
    {
    currentQualityIndex++;
    ApplyQualitySettings();
    }
    }
    }

    private void ApplyQualitySettings()
    {
    QualitySettings settings = qualityLevels[currentQualityIndex];

    // Apply quality settings to game systems
    // Implementation depends on your specific game systems
    }
    }

Player Experience Optimization

  1. Loading and Transitions:

    • Implement asynchronous loading to prevent freezing.
    • Design engaging loading screens with tips or mini-games.
    • Use scene transitions that fit your game's aesthetic.
    public class SceneLoader : MonoBehaviour
    {
    [SerializeField] private GameObject loadingScreen;
    [SerializeField] private Slider progressBar;
    [SerializeField] private Text loadingTip;
    [SerializeField] private string[] loadingTips;

    public void LoadScene(string sceneName)
    {
    StartCoroutine(LoadSceneAsync(sceneName));
    }

    private IEnumerator LoadSceneAsync(string sceneName)
    {
    // Show loading screen
    loadingScreen.SetActive(true);

    // Display random tip
    loadingTip.text = loadingTips[Random.Range(0, loadingTips.Length)];

    // Start async loading
    AsyncOperation asyncLoad = SceneManager.LoadSceneAsync(sceneName);
    asyncLoad.allowSceneActivation = false;

    // Update progress bar
    while (asyncLoad.progress < 0.9f)
    {
    progressBar.value = asyncLoad.progress;
    yield return null;
    }

    // Loading is technically done at this point (0.9)
    progressBar.value = 1.0f;

    // Wait for any additional loading screen animations
    yield return new WaitForSeconds(0.5f);

    // Activate the scene
    asyncLoad.allowSceneActivation = true;

    // Hide loading screen after transition
    while (!asyncLoad.isDone)
    {
    yield return null;
    }

    loadingScreen.SetActive(false);
    }
    }
  2. Save Systems:

    • Implement frequent auto-saves and clear manual save options.
    • Design save systems that are resistant to corruption.
    • Create informative save/load UI with timestamps and screenshots.

Conclusion

Game design is both an art and a science, requiring creativity, technical knowledge, and an understanding of player psychology. By following these best practices, you can create games that are not only technically sound but also engaging, accessible, and enjoyable for your target audience.

Remember that these guidelines are not rigid rules—game design often requires breaking conventions to create innovative experiences. Use these best practices as a foundation, but don't be afraid to experiment and develop your own unique design philosophy as you grow as a game developer.

As you implement these practices in Unity, you'll develop a deeper understanding of how code and design work together to create compelling interactive experiences. The most successful games seamlessly blend technical excellence with thoughtful design to create memorable player experiences.