Design patterns for Minecraft games with RedScript
Writing game logic for Minecraft datapacks is hardest not because of syntax, but because we need to fit behavior into ticks, scoreboard, NBT, and function file trees. The value of RedScript is exactly that it turns many classic game patterns into immediately usable code structures. Looking through ../redscript/examples/, you can see several very stable patterns.
1. State machines: the first pattern to learn
examples/enum-demo.mcrs is the most standard example. It defines enum Phase { Idle, Moving, Attacking }, stores phase, ticks, and active in a NpcState struct, then dispatches in @tick fn npc_tick() by match npc.phase to phase_idle(), phase_moving(), and phase_attacking(). This approach has three benefits: state is explicit; transition conditions are centralized; each state handler stays small.
The same idea is clearer in examples/rpg/health_system.mcrs and examples/rpg/boss_fight.mcrs. The former uses PlayerStatus as Alive/Wounded/Dead, while the latter uses BossPhase to manage a three-phase boss fight. For datapacks, state machines are almost a first principle because tick execution is discrete—any phase-based gameplay should be modeled as enums and transitions first, instead of spreading dozens of boolean flags.
2. ECS: stop piling fields when structs become many
src/stdlib/ecs.mcrs provides a lightweight ECS model: component state as int[], presence via tags, scalar fields in scoreboards. For instance in a health component layout, [1] stores current HP and [2] stores max HP; the velocity component stores vx/vy/vz in fixed slots. It does not aim for object-oriented methods, and instead emphasizes data layout and batched processing.
Interesting enough, examples like tower_defense.mcrs and racing.mcrs are still mainly in a handwritten struct style: Tower, WaveState, TDState, RaceState are modeled directly. This is very readable at small scale, but when entity types, optional attributes, and system count grow, ECS becomes the better path. In other words, examples show a clear evolution from “structured state” to “componentized state,” and ecs.mcrs gives you a stable expansion target.
3. Event-driven: avoid stuffing everything into @tick
src/stdlib/events.mcrs is straightforward: in @tick, it polls signals like deathCount, totalKillCount, and tag joins, then dispatches into function tags like #rs:on_player_join and #rs:on_player_death. At the type layer, src/events/types.ts constrains handler parameters for PlayerJoin, PlayerDeath, EntityKill, and ItemUse.
This pattern is not about deleting polling entirely; it wraps polling into an event interface. For game logic authors, writing @on(PlayerDeath) is more controllable than maintaining a dozen scoreboard objectives by hand. In real projects, it is usually best to route naturally asynchronous logic—state progression, drops, achievements, UI hints—to event entry points instead of making one huge game_tick() do everything.
4. scheduler: make “do this at a future tick” explicit
There are two scheduler styles in RedScript. examples/scheduler-demo.mcrs demonstrates compiler-level delayed calls with @schedule(ticks=20): it compiles to _schedule_xxx wrappers and issues schedule function commands directly. This is best for one-off future tasks, such as rewards after 1 second, switching to daytime after 5 seconds, or chaining phase transitions.
The second style is the scoreboard scheduler in src/stdlib/scheduler.mcrs. It offers task_schedule, task_ready, and scheduler_tick, treating 8 player slots plus 8 global slots as countdown buckets. This version is better for ongoing systems like cooldowns, wave intervals, or repeated boss skill loops. tower_defense still uses hand-written spawn_timer and tick % 60, but if task count keeps growing, migrating to scheduler gives better stability.
5. singleton: when you need world-level state
Although many examples still use plain global let or single struct variables, @singleton is the clearer global-state pattern from a language perspective. Tests show it synthesizes Type::get() / Type::set() for a struct, and fields land on a dedicated scoreboard objective. This pattern is suitable for lobby state, global countdowns, current level, matching queues, and any data that has only one instance.
If we further refactor npc in enum-demo, boss in boss_fight, and td/wave in tower_defense, a natural direction is to move these singleton-like states to @singleton structs, while player/entity private state remains in selectors/scoreboards or ECS components. That boundary is cleaner: singleton for world state, ECS for entities, state machines for phases, scheduler for time.
Conclusion
Writing Minecraft games with RedScript is less about memorizing APIs and more about picking the right pattern. Small-scale gameplay can start with state machines; switch to ECS as entities grow; use event-driven flow for asynchronous triggers; assign cross-tick future actions to scheduler; and keep only one-instance world data in singleton. The examples/ patterns already show one clear thing: even on a datapack-level platform, if your pattern choice is right, your code can stay a real game-language implementation rather than an unmaintainable command script.