Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 186 additions & 0 deletions Assets/DebugOverlay/CVar.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
using System;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// Quake-style configuration variables ("CVars").
/// These are intended for very cheap runtime access (one static field read)
/// while still being discoverable and editable from the in-game console.
/// </summary>

#region Registry

/// <summary>
/// Static global registry of all CVars. Every CVar registers itself in its
/// constructor. The registry is only used by the console or tooling – normal
/// runtime code never goes through it.
/// </summary>
public static class CVarRegistry
{
static readonly Dictionary<string, CVarBase> s_Map = new Dictionary<string, CVarBase>(StringComparer.OrdinalIgnoreCase);
static readonly List<CVarBase> s_List = new List<CVarBase>();

internal static void Register(CVarBase v)
{
if (s_Map.ContainsKey(v.Name))
{
Debug.LogWarning($"CVar '{v.Name}' already registered – ignoring duplicate.");
return;
}
s_Map.Add(v.Name, v);
s_List.Add(v);
}

public static bool TryGet(string name, out CVarBase v) => s_Map.TryGetValue(name, out v);
public static IEnumerable<CVarBase> All => s_List;

/// <summary>
/// Adds console commands for every registered CVar and the helper "cvar" command.
/// Call this once after the Console instance is created.
/// </summary>
public static void RegisterConsoleCommands(Console console)
{
// Per-CVar command: same name as the variable.
foreach (var cvar in s_List)
{
var local = cvar; // avoid modified-closure issue
console.AddCommand(local.Name, (args) => local.HandleConsoleCommand(args), local.Description);
}

// Helper command: "cvar list" (extend with save/load in the future).
console.AddCommand("cvar", (args) => CmdCVar(console, args), "cvar list – list all CVars");
}

static void CmdCVar(Console console, string[] args)
{
if (args.Length == 0 || string.Equals(args[0], "list", StringComparison.OrdinalIgnoreCase))
{
foreach (var v in s_List)
console.Write(" {0,-15} = {1} {2}\n", v.Name, v.ValueString, v.Description);
return;
}

console.Write("Unknown cvar argument. Try 'cvar list'\n");
}
}
#endregion

#region Base class
/// <summary>
/// Non-generic abstract base so that the registry can store different CVar
/// types in one collection.
/// </summary>
public abstract class CVarBase
{
public readonly string Name;
public readonly string Description;

protected CVarBase(string name, string description)
{
Name = name;
Description = description;
CVarRegistry.Register(this);
}

public abstract string ValueString { get; }
public abstract void SetValueFromString(string value);

internal void HandleConsoleCommand(string[] args)
{
if (args.Length == 0)
{
Game.console.Write("{0} = {1}\n", Name, ValueString);
return;
}

// Accept syntaxes: "var 123", "var = 123"
int valueIndex = 0;
if (args[0] == "=")
{
if (args.Length < 2)
{
Game.console.Write("Expected value after '='\n");
return;
}
valueIndex = 1;
}
SetValueFromString(args[valueIndex]);
Game.console.Write("{0} set to {1}\n", Name, ValueString);
}
}
#endregion

#region Primitive specialisations
public sealed class CVarFloat : CVarBase
{
float _value;
public CVarFloat(string name, float defaultValue, string description = null) : base(name, description)
{
_value = defaultValue;
}

public float Value => _value;
public static implicit operator float(CVarFloat v) => v._value;
public override string ValueString => _value.ToString();
public override void SetValueFromString(string s)
{
if (float.TryParse(s, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var f))
_value = f;
else
Game.console.Write("Could not parse float: {0}\n", s);
}
}

public sealed class CVarInt : CVarBase
{
int _value;
public CVarInt(string name, int defaultValue, string description = null) : base(name, description)
{
_value = defaultValue;
}
public int Value => _value;
public static implicit operator int(CVarInt v) => v._value;
public override string ValueString => _value.ToString();
public override void SetValueFromString(string s)
{
if (int.TryParse(s, out var i))
_value = i;
else
Game.console.Write("Could not parse int: {0}\n", s);
}
}

public sealed class CVarBool : CVarBase
{
bool _value;
public CVarBool(string name, bool defaultValue, string description = null) : base(name, description)
{
_value = defaultValue;
}
public bool Value => _value;
public static implicit operator bool(CVarBool v) => v._value;
public override string ValueString => _value ? "true" : "false";
public override void SetValueFromString(string s)
{
if (bool.TryParse(s, out var b))
_value = b;
else if (int.TryParse(s, out var i))
_value = i != 0;
else
Game.console.Write("Could not parse bool: {0}\n", s);
}
}

public sealed class CVarString : CVarBase
{
string _value;
public CVarString(string name, string defaultValue, string description = null) : base(name, description)
{
_value = defaultValue;
}
public string Value => _value;
public static implicit operator string(CVarString v) => v._value;
public override string ValueString => _value;
public override void SetValueFromString(string s) => _value = s;
}
#endregion
2 changes: 2 additions & 0 deletions Assets/DebugOverlay/CVar.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 24 additions & 0 deletions Assets/DebugOverlay/Console.cs
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,30 @@ void ExecuteCommand(string command)
}
else
{
// Support syntax like "name=value" or "name= value" for CVars
int eqIdx = commandName.IndexOf('=');
if (eqIdx > 0)
{
var potentialName = commandName.Substring(0, eqIdx);
if (m_Commands.TryGetValue(potentialName, out commandDelegate))
{
var rhs = commandName.Substring(eqIdx + 1);
// Build new argument list: if RHS not empty use it, otherwise use remaining split args
if (!string.IsNullOrEmpty(rhs))
commandDelegate(new string[] { rhs });
else if (splitCommand.Length > 1)
{
var args = new string[splitCommand.Length - 1];
Array.Copy(splitCommand, 1, args, 0, args.Length);
commandDelegate(args);
}
else
{
commandDelegate(Array.Empty<string>());
}
return;
}
}
Write("Unknown command: {0}\n", splitCommand[0]);
}
}
Expand Down
7 changes: 7 additions & 0 deletions Assets/Game/Game.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ public void Init()
m_Console = new Console();
m_Console.Init();

// Force static initialization of known CVars
_ = GameCVars.Fov;
_ = GameCVars.GodMode;

// Register variable commands (CVars)
CVarRegistry.RegisterConsoleCommands(m_Console);

m_Console.AddCommand("quit", CmdQuit, "Quit game");

m_Stats = new Stats();
Expand Down
11 changes: 11 additions & 0 deletions Assets/Game/GameCVars.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/// <summary>
/// Example CVars used by the game. Feel free to add more.
/// </summary>
public static class GameCVars
{
// Visuals
public static readonly CVarFloat Fov = new CVarFloat("fov", 60f, "Vertical field of view (degrees)");

// Gameplay toggles
public static readonly CVarBool GodMode = new CVarBool("god", false, "Player invulnerability toggle");
}
2 changes: 2 additions & 0 deletions Assets/Game/GameCVars.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

56 changes: 56 additions & 0 deletions DEVELOPMENT_GUIDELINES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Development Guidelines

This document contains guidelines extracted from analyzing preferred implementations to ensure future development aligns with the project's design philosophy.

## Core Principles

### 1. Prioritize Simplicity Over Abstraction
- **Prefer concrete classes over generic abstractions**
- **Avoid complex manager patterns when simple registration works**
- **Keep related functionality in single files when possible**
- **Write less code when possible**

### 2. Performance is Paramount
- **Direct field access over method calls for hot paths**
- **Minimize allocation and indirection**
- **Avoid implicit operators or complex conversion layers**
- **Prefer explicit over implicit behavior**

### 3. Follow Unity's Design Patterns
- **Use direct field access like Unity components do**
- **Keep registration simple and automatic**
- **Prefer concrete implementations over abstract interfaces**
- **Use clear, descriptive names**

### 4. Console Integration Should Feel Natural
- **Use intuitive command syntax (`name value` vs `name = value`)**
- **Query by name for getting values**
- **Keep parsing logic simple and direct**
- **Minimize complex assignment detection**

### 5. Code Style Guidelines
- **Write less code when possible**
- **Prefer concrete implementations over abstract interfaces**
- **Use clear, descriptive names**
- **Avoid over-engineering solutions**

### 6. File Organization
- **Keep related functionality together**
- **Prefer single files over multiple small files**
- **Minimize cross-file dependencies**

## Key Takeaways

1. **Simplicity wins**: The preferred implementation is much shorter and easier to understand
2. **Performance matters**: Direct field access (`cvar.value`) is faster than method calls
3. **Unity-like patterns**: Follow Unity's style of direct, explicit access
4. **Natural integration**: Console commands should feel intuitive and simple
5. **Less is more**: Avoid unnecessary abstraction layers and complex type systems

## When to Apply These Guidelines

- **New feature development**: Always consider the simpler approach first
- **Performance-critical code**: Prioritize direct access over abstraction
- **Console integration**: Keep commands natural and intuitive
- **Code reviews**: Question complex abstractions and prefer simpler solutions
- **Refactoring**: Look for opportunities to simplify existing code