Teddy Bear Trash Compactor
- 2025-10-28
That’s the rule of Chekhov’s Gun: Have a gun. Now it’s been seen, I will have to shoot someone before the end of the play...
If your game includes a teddy bear and a trash compactor, there’s a good chance one of your players is going to try to put that teddy bear in that trash compactor. Why? Because it’s a tiny, consequence-free act of pure nihilism. Because they want to see what will happen. Because some people just want to watch the world burn. The why isn’t actually important.
I bring up the Teddy Bear Trash Compactor scenario as an example of players making fun for themselves. These sorts of actions are interesting to me, as a game designer, because they are:
- specific choices, enacted intentionally and often me…
Teddy Bear Trash Compactor
- 2025-10-28
That’s the rule of Chekhov’s Gun: Have a gun. Now it’s been seen, I will have to shoot someone before the end of the play...
If your game includes a teddy bear and a trash compactor, there’s a good chance one of your players is going to try to put that teddy bear in that trash compactor. Why? Because it’s a tiny, consequence-free act of pure nihilism. Because they want to see what will happen. Because some people just want to watch the world burn. The why isn’t actually important.
I bring up the Teddy Bear Trash Compactor scenario as an example of players making fun for themselves. These sorts of actions are interesting to me, as a game designer, because they are:
- specific choices, enacted intentionally and often methodically by the player,
- the opportunity for which emerged naturally from the existing game world,
- perused despite having no mechanical reward, and no expectation of acknowledgment,
- which nevertheless makes the player feel something (wicked glee, shame, curiosity).
I think games should try as hard as they can to recognize and respond to actions like these. When you put the teddy in the compactor and an NPC nearby goes “(gasp) you’re a monster”, it’s funny and underscores the emotional impact. I love that shit.
At this point, we could go into Theories of Fun and why interactions like these make games special, but I’m going to save that for another article. This one is about the mechanics: How do you track the state of the game in a way that allows you to respond to stuff like this?
Data Organization & Behavior
Say you’re making a game: You’ve got some kind of global state, and then you’ve got a ton of “Game Objects” that might be bits of the map, or NPCs, or items. How should this data be organized? Well, it depends.
You have to consider how that data is likely to be used. What behavior is it going to drive? As we’ll see, different types of behavior call for different representations and organizations.
Consistency
One very common requirement for behavior is consistency. Take physics, for example. The laws of physics, I am given to understand, are pretty much the same everywhere. If you want all your game objects to have the same physics, you generally want to have a single physics implementation. That implementation should probably periodically iterate over your game objects to read and update their physics data.
Following that line of reasoning, we can draw some conclusions about data representation. Storing all the physics data in a big uniform array is probably a good choice. By contrast, giving each game object (or perhaps class of game object) custom physics properties is probably a bad idea. If some of your objects express their inertial moment in different units, or represent their position and orientation using quaternions instead of projective matrices, achieving uniform behavior is going to be harder.
Generally, to get consistent behavior, it’s best to have consistent data stored in a consistent place. This is all pretty textbook stuff.
Specificity
Sometimes though specificity matters more than consistency. My teddy bear trash compactor example is hyper-specific; there’s just one trash compactor and just one teddy bear, so consistency isn’t a concern at all.
In isolation, you can do whatever you want with inconsistent data. But what if you have several thousand specific state variables? What if they are all different types? At scale, even though it can be wildly inconsistent, highly specific behaviors have their own set of constraints. The main problem is ergonomics. The easier it is to store ad-hoc state, the more you will do so. So highly specific behavior requires effortless data definition and access.
In practice, there is often a trade-off between consistency and specificity. Consistent behavior affects more game objects, so you can afford to use patterns with more boilerplate, which are more complex to set up and maintain. Hyper-specific behaviors have no such luxury, since they may only apply to a single object for a brief moment. They are therefore constrained to patterns that cost next to nothing to set up and use.
Why am I talking about this? Well, over the past few months, I’ve gradually come to realize that this consistency/specificity tradeoff underpins some of the issues I have with the ECS pattern.
Dial E for Entity
You’re reading a blog about game engine design, so I assume you’ve heard of an ECS before. If not, ECS is a data-storage pattern often used in games. It’s an acronym.
- E is for Entity: A key that uniquely identifies a game object.
- C is for Component: A type that each game object may have an instance of.
- S is for System: A routine that operates on game objects with a certain set of components.
The ECS pattern is great for consistency. Every system defines behavior, and entities can opt in to behavior by adding a specific combination of components. Theoretically, an ECS also provides composition, with multiple isolated systems all working together to create complex emergent behavior across the various entities they operate on.
These benefits generally come at a cost: tracking the components attached to each entity comes with overhead, and setting up new components and systems often has some boilerplate. And, in my opinion, the typical approach to components tends to get in the way when implementing inconsistent or hyper-specific behaviors.
Naive Example
Let’s look at what it takes to implement Teddy Bear Trash Compactor in my ECS of choice (Bevy).
To start, we’ll need some way to identify a the teddy bear and trash compactor game objects. An ECS idiom exists for this called “marker components” (which carry no data).
#[derive(Component)]
struct TeddyBear;
#[derive(Component)]
struct TrashCompactor;
We also need a place to store the actual state, which in bevy probably belongs in a “resource” (which are effectively just containers for global data).
#[derive(Resource)]
struct IsReadyBearInTrashCompactor(bool);
This resource will need to be initialized when the game starts (so that we can persist it’s state between saves and so on) and that means we’ll have to register it in a plugin. The game in question probably already has one, so we’ll add it to that.
impl Plugin for MyGame {
fn build(&self, app: &mut App) {
...
app.init_resource::<IsReadyBearInTrashCompactor>();
...
}
}
Now in a system we can do the actual distance check and update the state resource.
impl IsReadyBearInTrashCompactor {
fn update(
mut self: ResMut<Self>
teddy_bear: Single<&Transform, With<TeddyBear>>,
trash_compactor: Single<&Transform, With<TrashCompactor>>,
) {
let dist = teddy_bear.translation.distance(trash_compactor.translation);
if dist < 1.0 {
self.0 = true;
}
}
}
This will also have to be schedule for execution within a plugin.
...
app.init_resource::<IsReadyBearInTrashCompactor>();
app.add_systems(PostUpdate, IsReadyBearInTrashCompactor::update);
...
Taken together you get this.
#[derive(Component)]
struct TeddyBear;
#[derive(Component)]
struct TrashCompactor;
#[derive(Resource)]
struct IsReadyBearInTrashCompactor(bool);
impl IsReadyBearInTrashCompactor {
fn update(
mut self: ResMut<Self>
teddy_bear: Single<&Transform, With<TeddyBear>>,
trash_compactor: Single<&Transform, With<TrashCompactor>>,
) {
let dist = teddy_bear.translation.distance(trash_compactor.translation);
if dist < 1.0 {
self.0 = true;
}
}
}
impl Plugin for MyGame {
fn build(&self, app: &mut App) {
...
app.init_resource::<IsReadyBearInTrashCompactor>();
app.add_systems(PostUpdate, IsReadyBearInTrashCompactor::update);
...
}
}
This is a fair amount of code for a comparatively simple feature! It also introduces a lot of types, which, if you have thousands of similar little interactions, means: slower compile times, difficulty finding relevant things, and so on. It’s just a lot of work to put on the compiler for very little benefit.
A Modest Proposal
Let me show you another way to write the same thing, from a parallel universe.
fn is_teddy_bear_in_trash_compactor(world: &mut World) -> Result<()> {
let teddy_bear = world.entity_named("teddy_bear")?.get::<Transform>()?;
let trash_compactor = world.entity_named("trash_compactor")?.get::<Transform>()?;
let dist = teddy_bear.translation.distance(trash_compactor.translation);
if dist < 1.0 {
world.set_property("is_teddy_bear_in_trash_compactor", true);
}
Ok(())
}
impl Plugin for MyGame {
fn build(&self, app: &mut App) {
...
app.add_systems(PostUpdate, is_teddy_bear_in_trash_compactor);
...
}
}
I’ve just done two things here:
- Replaced the static marker components with a dynamic name map.
- Replaced the static state resource with a dynamic properties map.
I think this is big improvement. Compared to the previous, this version is more self-contained. It also uses the type system less (which means potentially faster compiles) and fewer macros. Remember the scale here, with a few hundred marker components, things add up. A few hundred flags could be more than a thousand mostly pointless types, which we now have to manage, and wait to compile.
There’s also more flexibility here. Names could be defined entirely at runtime, if you wanted.
What’s The Downside?
What we have sacrificed, going from the first implementation to the second? Correctness. Suppose we change it from a teddy bear to a companion cube, or from a trash compactor to an incinerator. For the first version, the compiler is able to ensure that we make the change uniformly. In the second version, it’s on us to make sure we update all references to the name correctly.
Now, I don’t think reducing compiler checking here is a huge loss. There are some things the compiler just can’t check, and you wouldn’t want to have it check:
- Content. Having to define content, like dialog trees, within the Rust type system is cumbersome and annoying.
- Scene Editors. Since marker components are compiled into the app, they are more complex to embed in scenes. Name strings are easy for editors to work with.
- Scripting. Using a simple key-value store instead of a resource makes it trivial to read and write from scripts.
- Networking. If you’re writing a networked game, encoding the organization of game state dynamically rather than within the type system can allow the server to send custom state to the client.
There are just so many things you can do with dynamic patterns like these. What you lose in correctness, we make up in flexibility and interoperability.
My issue with many ECS implementations, which I alluded to earlier, is that they often do not include dynamic patterns or even discourage them. There is probably a slight selection bias among users of ECS-based engines (and especially Bevy), which draws people who like type-checking and correctness and large, consistent systems. To some extent, I think those same people tend to dislike exceptions, inconstancies, had-hoc behavior, and specificity. It’s fine to dislike these sorts of APIs, especially if your work doesn’t require them. But I think they are still important things for an engine to support.
Engines which discourage these patterns rob their designers of the ability to make consistency/specificity correctness/flexibility tradeoff decisions themselves. And focusing on static correct consistent patterns can make it more difficult to implement certain things. All the categories I listed (Content editing, Scripting, Networking) have historically been weak points for Bevy. Perhaps it is because we are simply using the wrong patterns.
Stay Tuned
Some of you may still be skeptical, and that’s fine. This post ended up being a lot of me “telling” and less “showing”. In my next post, I’m going to show a more concrete example of how all this runtime-dynamic stuff can be used to implement a pretty sweet rules-based response system (modeled off the Source Engine dialog system).
Until then, here’s an omen of things to come...
(criterion TeddyBearInTrashCompactor (is_teddy_bear_in_trash_compactor == true))
(rule (ConceptInteract Ally NpcIdle TeddyBearInTrashCompactor) (YouMonster))
(rule (ConceptInteract Ally IsLisa NpcIdle TeddyBearInTrashCompactor) (NeverLikedBears))
(response YouMonster list
(line "(gasp) you monster!"))
(response NeverLikedBears list
(line "I never liked bears anyway."))