Ix Dev 1 - Overview

Motivation

Tech

  • Extensive use of TypeScript for type safety
  • ReactJS for UI
  • ExcaliburJS for rendering
  • break-eternityjs for large number support (Decimal)
  • an NPM workspaces-based monorepo setup

Components

Common

The common package is my core library that I carry from project to project, containing helper functions and structures for web-based games. It houses game data structures (grids, FSMs, etc), react components, and my core style library to make it easy to start new projects.

Script

Ix-Script is a scripting language to support my game projects, with a monaco-based web editor, common library, and growing in-language test suite. I built this to make it easy and safe to script and extend games and UIs at runtime, allowing binding to JS-based objects with minimal overhead. In particular, most control flow structures are expressions, slimming down simple scripting.

Loading syntax highlighter...

mapgen

My library for generating 2d maps, using composable region builders and processes.

game

The game itself. This uses React for the UI (including in-game popups), with ExcaliburJS as the renderer. Excalibur has support for entities, actors, and systems, but I ignore those and instead use my own engine and datastructures for game logic.

game structure

For convenience, there's a single global $mine variable available throughout the program. Game state is managed via the $mine.state object, containing the full persistable state of the game (some UI-only concerns are kept in React state and not serialized). The engine property is the root Excalibur engine, which handles rendering the in-game scenes and binding visual Actors to my game's Entity system.

Game logic - including handling (abstracted) inputs, and showing or hiding "modal" windows - is run every Excalibur frame.

For performance, the state object is mutable. This provides a bit of a performance challenge for React - my solution is:

  1. a single state.cacheIdx variable, incremented when something "significant" or user-triggered happens
  2. a custom useGameSelector(state => getSomethingFromState, {cacheBy: 'tick' | 'cacheIdx', deepCompare?: boolean}) hook (inspired by redux and the like), which only re-checks the value every 100ms or cache increment and can optionally serialize to check for differences
  3. careful control over those flags for each UI component

This allows the game to update at eg 60fps, but the UI components to only check for stale data much less frequently - reducing the cost of the selectors even before factoring in the cost of a render cycle.