bevyengine / bevyengine/bevy

`Entity::PLACEHOLDER` should not be used as a null value

Open
#25,118 3 comments 1 reaction 0 assignees View on GitHub
A-ECS C-Code-Quality S-Ready-For-Implementation X-Contentious
Dominant language
Rust
Stars
48.2k
Forks
4.8k
Avg merge
3d 22h
Merged PRs (30d)
161

Description

The documentation for [`Entity::PLACEHOLDER`](https://docs.rs/bevy_ecs/0.19.0/bevy_ecs/entity/struct.Entity.html#associatedconstant.PLACEHOLDER) says:

```rust
/// An entity ID with a placeholder value. This may or may not correspond to an actual entity,
/// and should be overwritten by a new value before being used.
```

This implies that `Entity::PLACEHOLDER` should not be used as a null value - i.e. nothing should be checking `if entity == Entity::PLACEHOLDER`, or relying on `world.get(Entity::PLACEHOLDER)` to return not found. So the only purpose of `Entity::PLACEHOLDER` is to temporarily work around `Entity` not supporting `Default` (e.g. initializing a `[Entity; N]`).

But there are a number of places in the engine that use `Entity::PLACEHOLDER` as a null value (e.g. `ComputedUiTargetCamera::get`). There's also some code that can pass it to `World::get` (e.g. `propagate_ui_target_cameras`), or doesn't have guard rails to prevent other code doing that by accident. These cases should try to avoid `Entity::PLACEHOLDER` by using `Option` or more enum variants.

## Is This Really A Bug?

Reference types that require supporting a null value are generally seen as a bad thing - they make it harder to understand the intent and scope of code (see [The Billion Dollar Mistake](https://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare/)). And there have been previous decisions to avoid `Entity::PLACEHOLDER` (#16029).

But there are some counter-arguments. An `Entity` on its own still has ambiguous intent - it's not like a `Box` that's guaranteed to point to something valid. The entity might be despawned, or might not have an expected component. So there's an argument that `Option` just adds complexity and ambiguity, and that `Entity::PLACEHOLDER` should become an official null value (#16204).

Personally, I think `Option` is useful *if used consistently*. That means an `Entity` on its own is a strong hint - although not a guarantee - that the referenced entity was valid when the variable was initialized. In contrast, an `Option` clearly says that there might never have been a referenced entity.

For example, take this well-dressed player component:

```rust
#[derive(Component)]
struct Player {
monocle: Option
top_hat: Entity,
}
```

This communicates that the player's monocle is optional, but they should at least start with a top hat.

## How Difficult Is The Fix?

Most cases that I could see are straightforward, but there's at least a few awkward ones. I didn't do a thorough investigation so I might have missed other cases.

The most fiddly case is where the renderer stores a `MainEntity`/`RenderEntity` pair, and one of the pair can in some cases be null (e.g. [`BinnedRenderPhaseBatch::representative_entity`](https://docs.rs/bevy_render/0.19.0/bevy_render/render_phase/struct.BinnedRenderPhaseBatch.html#structfield.representative_entity)). There's a lot of code to fix up and it's not always clear where null is allowed. And I suspect there could be issues where entities are stored in `EntityHashMap`, which would mean a new type is needed to to support `Option`.

Another awkward case is one-to-one relationship targets (see `impl RelationshipSourceCollection for Entity`), which rely on `Entity::PLACEHOLDER` to represent a missing source entity. `Option` was considered but rejected (https://github.com/bevyengine/bevy/pull/18087#issuecomment-2696545889).

## Performance Of `Entity::PLACEHOLDER` Versus `Option`

`Entity` and `Option` are both 8 bytes (since #18704), so there's no memory concerns in most cases. There is a difference in the throughput of entity queries - this favors `Option` in most but not all situations.

For background, queries that take an `Entity` usually start by finding the location of the entity via `Entities::get_spawned`. That starts by checking if `Entity::index()` is within the bounds of the `Entities::meta` array. `Entity::PLACEHOLDER` will always be out of bounds, so it's sort of a free null check. Switching to `Option adds a branch to check for `None`, but on the other hand this branch is simpler and earlier than the bounds check.

The table below shows cycle counts for calling `Entities::get_spawned` in a loop.

| | `PLACEHOLDER` | `Option` | Throughput delta |
|-|-|-|-|
| 0% null | 4.14 | 4.52 | x0.92 |
| 50% null (unpredictable) | 17.09 | 11.73 | x1.46 |
| 50% null (predictable) | 3.92 | 2.67 | x1.47 |
| 100% null | 3.88 | 1.00 | x3.88 |

So `Entity::PLACEHOLDER` has a small advantage if all values are non-null. But `Option` will start to win as the number of nulls increases, and is much faster when all values are null.

Note that `Entity::get_spawned` is a very simple query - the `Entity::PLACEHOLDER` advantage in the non-null case will be proportionally lower in more typical queries. For example, `World::get` is roughly 19 cycles, so I'd estimate that `Option` would be ~2% slower.

(Tested on desktop Zen 4. I'll make a PR with the benchmark.)

## Can `Entity::PLACEHOLDER` Be Removed?

The case for `Option` would be stronger if `Entity::PLACEHOLDER` was removed entirely. That would make it harder (although still possible) to create a "null" entity, so `Entity` would be slightly less ambiguous. But there's a few problems.

First, there's a legitimate need for `Entity::PLACEHOLDER` when it's difficult to allocate and initialize an `Entity` variable in one step. This doesn't happen often, but there are a couple of cases in the engine:

1. Relationship sources (like `ChildOf`) want `Default` or `FromWorld` to support reflection.
2. [`PropagateEntityTrigger::original_event_target`](https://docs.rs/bevy_ecs/0.19.0/bevy_ecs/event/struct.PropagateEntityTrigger.html#structfield.original_event_target) wants `Default`.

I don't know enough about the relationship and event systems to say if these cases are solvable without major downsides. And of course there will be other situations outside of the engine - particularly components that want `Reflect`.

Second, there are cases where converting null values to `Option` will be awkward. One-to-one relationship targets are probably the main blocker (see earlier note).

Third, there are a bunch of tests that use `Entity::PLACEHOLDER` when something has an `Entity` variable that isn't relevant to the test. These are easy to fix but will add some boilerplate.

If `Entity::PLACEHOLDER` stays then its use as a null value could be discouraged by making the name more obnoxious (e.g. `Entity::TEMPORARY_PLACEHOLDER`). Another possibility is changing `World::get` and other entity queries to `debug_assert!(entity != Entity::PLACEHOLDER)`, making it harder to use as an implicit null.

## Summary

- There's a decent case to be made for avoiding `Entity::PLACEHOLDER` as a null value, but it's not a slam-dunk.
- The performance of `Option` is usually better, but there are situations where `Entity::PLACEHOLDER` has an edge.
- Removing `Entity::PLACEHOLDER` entirely seems technically possible but unlikely.

Contributor guide

Open the contributing guide

Research direction

Start by auditing the uses of Entity::PLACEHOLDER named in the issue, including ComputedUiTargetCamera::get, propagate_ui_target_cameras, BinnedRenderPhaseBatch::representative_entity, and relationship targets. Read the linked decisions around #16029, #16204, and pull request #18087 before choosing a direction. Done means null semantics are made explicit where appropriate without breaking legitimate placeholder uses or relationship behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
game-dev
Issue type
Refactor
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Needs clarification
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.