From 16d9cc153ee7282321ff92e4544ef771b33bbf11 Mon Sep 17 00:00:00 2001 From: Peter Andreasen Date: Wed, 30 Jul 2025 11:09:07 +0200 Subject: [PATCH 1/2] Guidelines for development. --- DEVELOPMENT_GUIDELINES.md | 56 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 DEVELOPMENT_GUIDELINES.md diff --git a/DEVELOPMENT_GUIDELINES.md b/DEVELOPMENT_GUIDELINES.md new file mode 100644 index 0000000..b66c3c8 --- /dev/null +++ b/DEVELOPMENT_GUIDELINES.md @@ -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 \ No newline at end of file From cc6fde1f862b30366d62f81b70abcbab99efb543 Mon Sep 17 00:00:00 2001 From: Peter Andreasen Date: Wed, 30 Jul 2025 11:27:32 +0200 Subject: [PATCH 2/2] Cvar done by o3 --- Assets/DebugOverlay/CVar.cs | 186 +++++++++++++++++++++++++++++++ Assets/DebugOverlay/CVar.cs.meta | 2 + Assets/DebugOverlay/Console.cs | 24 ++++ Assets/Game/Game.cs | 7 ++ Assets/Game/GameCVars.cs | 11 ++ Assets/Game/GameCVars.cs.meta | 2 + 6 files changed, 232 insertions(+) create mode 100644 Assets/DebugOverlay/CVar.cs create mode 100644 Assets/DebugOverlay/CVar.cs.meta create mode 100644 Assets/Game/GameCVars.cs create mode 100644 Assets/Game/GameCVars.cs.meta diff --git a/Assets/DebugOverlay/CVar.cs b/Assets/DebugOverlay/CVar.cs new file mode 100644 index 0000000..32aa76e --- /dev/null +++ b/Assets/DebugOverlay/CVar.cs @@ -0,0 +1,186 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +/// +/// 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. +/// + +#region Registry + +/// +/// 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. +/// +public static class CVarRegistry +{ + static readonly Dictionary s_Map = new Dictionary(StringComparer.OrdinalIgnoreCase); + static readonly List s_List = new List(); + + 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 All => s_List; + + /// + /// Adds console commands for every registered CVar and the helper "cvar" command. + /// Call this once after the Console instance is created. + /// + 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 +/// +/// Non-generic abstract base so that the registry can store different CVar +/// types in one collection. +/// +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 diff --git a/Assets/DebugOverlay/CVar.cs.meta b/Assets/DebugOverlay/CVar.cs.meta new file mode 100644 index 0000000..1dd8f5e --- /dev/null +++ b/Assets/DebugOverlay/CVar.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 4b93b9209f3fbe74a8a9aea98ebb6970 \ No newline at end of file diff --git a/Assets/DebugOverlay/Console.cs b/Assets/DebugOverlay/Console.cs index 944080d..bd42518 100644 --- a/Assets/DebugOverlay/Console.cs +++ b/Assets/DebugOverlay/Console.cs @@ -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()); + } + return; + } + } Write("Unknown command: {0}\n", splitCommand[0]); } } diff --git a/Assets/Game/Game.cs b/Assets/Game/Game.cs index 1ba3bdc..80b1250 100644 --- a/Assets/Game/Game.cs +++ b/Assets/Game/Game.cs @@ -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(); diff --git a/Assets/Game/GameCVars.cs b/Assets/Game/GameCVars.cs new file mode 100644 index 0000000..ddbbe80 --- /dev/null +++ b/Assets/Game/GameCVars.cs @@ -0,0 +1,11 @@ +/// +/// Example CVars used by the game. Feel free to add more. +/// +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"); +} diff --git a/Assets/Game/GameCVars.cs.meta b/Assets/Game/GameCVars.cs.meta new file mode 100644 index 0000000..0d622cc --- /dev/null +++ b/Assets/Game/GameCVars.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: fec9699f5a6105148a4740bcfaffc027 \ No newline at end of file