Git Product home page Git Product logo

cvars's Introduction

Cvars

Configuration variables .rs
A simple and ergonomic way to store and edit configuration at runtime

GitHub Docs.rs Crates.io License (AGPLv3) CI Audit Dependency status Crates.io Discord

Cvars (console variables or configuration variables) are a simple way to store settings you want to change at runtime without restarting your program.

Consoles are the most ergonomic way to set cvars but you can write your own UI or read them from stdin if you want. They are available for Fyrox and Macroquad.

These crates are inspired by the idTech (Doom, Quake) and Source family of game engines but they can be useful outside games. Cvars allow you to iterate faster by letting you test certain gameplay changes without recompiling. They also make your game more moddable if you expose (a subset of) them to players.

TL;DR: Set and get struct fields based on the field's name as a string. User writes the cvar's name and new value into the console, it sets the appropriate field in your config struct and the game now behaves differently. Your gamecode uses cvars as regular staticly typed values.

cvars-usage-rec-wars.mp4

Zero boilerplate - there are no traits to implement manually and no setup code to call per cvar.

Minimal performance cost - just struct field access instead of a hardcoded constant. Cvars are cheap enough to keep everything configurable even after you're done finding the best values - you can keep things tweakable in your released game for players to experiment themselves.

Usage

  • Add cvars to your Cargo.toml:
cargo add cvars
  • Put your config in a struct and derive SetGet:
use cvars::cvars;

// This generates a Cvars struct containing all your config options
// and a corresponding Default impl.
cvars! {
    g_rocket_launcher_ammo_max: i32 = 20,
    g_rocket_launcher_damage: f32 = 100.0,
}

// Store this in your game state.
let mut cvars = Cvars::default();
  • Allow users to change the config:
// These normally come from the user
// (from stdin / your game's console / etc.)
let cvar_name = "g_rocket_launcher_damage";
let new_value = "150";

// This looks up the right field and sets it to the new value.
cvars.set_str(cvar_name, new_value).unwrap();

Motivation

A player/modder/gamedev wants rockets to do more damage. He types g_rocket_launcher_damage 150 into the game's console or stdin. The code gets both the cvar name and new value as strings so you can't write cvars.g_rocket_launcher_damage = 150. You need to look up the correct field based on the string - this is what cvars does - it generates set_str (and some other useful methods). You call cvars.set_str("g_rocket_launcher_damage", "150"); which looks up the right field, parses the value into its type and updates the field with it. From then on, rockets do 150 damage.

The important thing is that in the rest of your application, you can still access your cvars as regular struct fields - e.g. player.health -= cvars.g_rocket_launcher_damage;. This means you only need to use strings when the user (player or developer when debugging or testing a different balance) is changing the values. The rest of your gamelogic is still statically typed and using a cvar in gamecode is just a field access without any overhead.

A typical game will have hundreds or thousands of tunable parameters. With cvars and a console you can keep them all configurable for advanced players, modders and your-gamedev-self without having a build an elaborate settings menu. You can keep everything configurable using a TUI while also exposing common settings to normal players in your game's GUI.

See cvars/examples/stdin.rs for a small runnable example.

Real-world examples

Look at games using cvars:

Press ; to open the console. Shift+ESC also works in the native clients but not in the browser.

GitHub Docs.rs Crates.io License (AGPL3) CI Audit Dependency status Discord

The Fyrox console is a separate crate in this repo. To use it in your game, add it to your Cargo.toml and call its methods on the relevant engine events.

Fyrox console

See the crates.io page or its docs for more information.

GitHub Docs.rs Crates.io License (AGPL3) CI Audit Dependency status Discord

The Macroquad console is a separate crate in this repo. To use it in your game, add it to your Cargo.toml and call its update method every frame.

Macroquad console

See the crates.io page or its docs for more information.

Features

  • Derive macro SetGet to create settters and getters for cvars based on their name
    • Statically typed (set, get)
    • As string (set_str, get_string)
  • Function like cvars! macro to declare type and initial value on one line
  • Support user-defined cvar types (both structs and enums)
  • Saving and loading cvars to/from files - useful if your game has multiple balance presets
  • In-game console for the Fyrox engine
  • In-game console for the Macroquad engine
  • Autocompletion

Features I am currently not planning to implement myself but would be nice to have. I might accept a PR if it's clean and maintainable but it's probably better if you implement them in your own crate:

  • In-game console for the Bevy engine
  • In-game console for the Egui UI toolkit
  • Non-blocking stdio-based console
    • This would bring the full power of cvars to any program that can access stdin/out without the need to implement a console for every engine or UI toolkit.

Alternatives

  • inline_tweak
    • Uses hashmaps - overhead on every access
    • Can't be used in some contexts (e.g. in a const)
    • Veloren switched to it from const-tweaker
  • const-tweaker
    • Web GUI
    • Only supports a few stdlib types, no custom types
    • Has soundness issues according to tuna's author
    • Uses hashmaps - overhead on every access
  • tuna
    • Web GUI
    • Unclear if it supports enums
    • Uses hashmaps - overhead on every access
  • cvar
    • Uses a trait instead of a macro. The trait seems to need to be implemented manually so more boilerplate.
    • Has additional features (lists, actions) which cvars currently doesn't.

Compared to these, cvars either has no overhead at runtime or requires less setup code. The downside currently might be slightly increased incremental compile times (by hundreds of milliseconds).

Cvars also serves a slightly different purpose than inline_tweak and const-tweaker. It's meant to stay in code forever, even after releasing your game, to enable modding by your game's community.

Finally, you might not need a specialized crate like cvars or inline_tweak at all. A lot of common wisdom in gamedev is wrong or outdated. What you need might be just a file containing RON or JSON which is loaded each frame and deserialized into a config struct. It'll be cached by the OS most of the time and nobody minds a dropped frame during development after editing the file.

Development

The repo is organized as a cargo workspace for the main functionality, with consoles and benchmarks as separate crates not part of the workspace - see Cargo.toml for the technical reasons.

  • Testing: Use cargo test in the root directory to test everything in the workspace. To test the consoles, cd into their directories and run cargo test there.

  • Debugging:

    • Use cargo expand --package cvars-macros --example testing-fnlike to see what the proc macros generate. There is a similar file for derive macros. You can use println! and dbg! in the macros as well.
    • The expanded code won't compile but the end of the output will usually contain errors that can help you track down what's wrong with the generated code: cargo expand --package cvars-macros --example testing-fnlike > cvars-macros/examples/testing-expanded.rs && cargo build --package cvars-macros --example testing-expanded ; rm cvars-macros/examples/testing-expanded.rs. One exception is when the macro produces syntactically invalid code, in which case its output will be missing entirely.
  • Benchmarking: Run ./bench.sh in cvars-bench-compile-time to benchmark incremental compile time when using the proc macros.

  • Useful commands:

    • cargo-llvm-lines and cargo-bloat. Use either of them in cvars-bench-compile-time (e.g. e.g. cargo llvm-lines --features string,typed,fnlike,cvars-1000) to find out which functions generate a lot of LLVM IR and which compile to a lot of code. This is a good indicator of what is causing long compile times. Lines of LLVM IR is a bit more important because it better indicates how much work the backend has to do even if it compiles down to a small amount of machine code.
    • Set the environment variable CVARS_STATS to make the macros print how long they took - e.g. CVARS_STATS= cargo bloat --features string,typed,fnlike,cvars-1000. If it's small compared to the total compile time, most of the time is spent in codegen, dealing with the large amount of code generated. Note that the compiler's output, including proc macro output, is cached if the compiled code hasn't changed so you might need to set the variable and also edit the code to see the stats.

Lessons Learned

  • If benchmarks seem to give you values much better (or worse) than normal usage, they're probably faulty. Rustc does a lot of caching, you have to edit the code when benchmarking recompile times (duh). But which file you edit and where makes a massive difference. With 1k cvars, adding a comment to the end of main.rs takes only 600 ms; editing the cvars struct causes a much bigger recompile and takes 6 s - 10x longer. Currently it appears rustc recompiles everything where line numbers changed - adding a line to the beginning of a file is more costly than the end. Alternative compilers like cranelift try to be smarter about this. The rules about when a macro is rerun and when its output is actually recompiled by the backend are also not obvious. For example editing a comment inside a macro's input without changing any line numbers forces it to be recompiled even though AFAIK this can't affect the macro's output in any way.
  • Minimize the amount of generated code. Proc macros are often not slow because of the macro's code itself but because they generate a lot of code which is turned into a lot of LLVM IR which takes the backend a lot of time to chew through. For example the case mentioned above spends only 70 ms in macro code, the rest of the 6 s is codegen. Extracting repeated code to functions gives a 1.5-3.0x speedup because a function call takes fewer lines of LLVM IR.
  • Not all code is created equal. 10k offsets in a static phf HashMap don't show up anywhere in cargo-llvm-lines and barely affect compile times compared to a 10k line match statement which takes minutes to compile.
  • You might not need to write a specialized lib if you can (mis)use an existing one that does almost what you need. Cvars offer a slightly more convenient way of adding cvars compared to a struct with derived Serialize/Deserialize and a reflection crate. However they took me a lot more time to implement and they will only start paying off once my games have working multiplayer and even then the most important part will be the consoles, not the cvars macros themselves.

Contributing

You can always find me on the Rusty Games Discord server if you have any questions or suggestions.

Issues and Pull Requests are welcome. See the good first issue label for easy tasks.

License

AGPL-v3 or newer

cvars's People

Contributors

dependabot[bot] avatar martin-t avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

cvars's Issues

Compile times

Switch to a hashmap or trie for storing the strings, the generated match is huge:

cargo llvm-lines:

  Lines          Copies       Function name
  -----          ------       -------------
  119771 (100%)  4182 (100%)  (TOTAL)
   30067 (25.1%)    1 (0.0%)  rec_wars::cvars::Cvars::set_str
    5892 (4.9%)     1 (0.0%)  rec_wars::cvars::Cvars::get_string
    3083 (2.6%)     1 (0.0%)  rec_wars::rendering::render_viewport

and render_viewport is a huge 700 line function.

Maybe llvm lines are not the best metric but it absolutely does add a lot to compile times (test with SetGetDummy)

Attribute for skipping a field?

I've got some structs with fields that must not be changed (would cause crashing and/or freezing), but would like to keep the remaining fields configurable...

Would it be possible to add something like a #[cvars(skip)] attribute, similar to #[serde(skip)]? Maybe check for the latter too?

Compare perf with alternatives

Compare normal access in gamecode and stringly-typed access when the user is accessing the config. Small and large amount of cvars.

no_std compatibility

There are almost no allocations, might be worth seeing if it's possible to make it no_std compatible.

Would have to conditionally remove get_string since String allocates.

Add support for `pub(crate)`

Currently we hardcode pub in the generated code, which means making the Cvars struct pub(crate) gives an error about leaking a less visible type in a public interface.

Feature: Nested structs

  • cvars_structs! {} to allow defining whole structs that contain each other
  • Useful for configuring multiple weapons/vehicles/items/etc that have the same fields - rec-wars does it manually atm.

Feature: Commands

  • Built-in - help, cvarlist, search/find/apropos, cmdlist ...
  • User-defined - just call a hook function and that returns true if handled? Would be nice to be a bit more declarative so they can be listed in help/cmdlist. Avoid making it overcomplicated - define_command function that takes the help message and the hook?

Integer ID's?

Would it be possible to have a pair of set_var and get_var-functions that use an integer-index (parallel to str), obtainable by some get_idx function? Maybe also have a macro that can resolve this at compile-time?

This could be implemented by wrapping the memory-offset (using https://crates.io/crates/memoffset ?) in a CvarHandle<T> {offset: u16} struct with an impl PartialEq, where T is the source-type... thus never exposing the actual offset to the library user.

CI

Check for:

  • dead links
  • FIXMEs
  • typos?

Try it out link

When RecWars console works sanely or RustCycles supports WASM, add a heading "Try it out" right before usage that explains how to open the console and which cvars to try changing. Maybe reduce number of bots?

Console features

  • Tab - autocompletion if unambiguous, print possibilities if ambiguous
  • Write text, up arrow -> search history only for items starting with the text
    • Pg up/down still walks history normally (like zsh)
  • cvarlist
  • find/search/apropos with multiple words - search for those words appearing in any order
  • If starting with space, don't save to history
  • History to file
  • CTRL+R - fuzzy search in history - possible to use fzf or alternative (atuin/skim)?

Aliases

Aliases would allow users to save typing and one cvar could have multiple names for users coming from different engines used to different names.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.