-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Description
Caveat: the ideas presented here are fairly wild / brainstormy and I don't expect them to be accepted or implemented - but I do want to write them down here because they may inspire discussion.
I'm in the process of porting my game engine and editor from three.js to Bevy. Three.js has about a dozen Gizmos (which are called "Helpers" in three.js terminology), but I don't find them to be particularly useful for my needs. Instead, I've created my own Gizmo framework which has gone through several iterations. I don't use these in the game per se, but they are very helpful in the editor for things like:
- Terrain editing
- Physics colliders
- Musical and sound effects cues
- NPC pathfinding waypoints
- Navigation meshes
- Portal apertures
- Water current direction
- Use markers (named markers that tell the actor where to stand when interacting with a scenery element).
...and many others. There are about two dozen custom gizmo types that I use, all of which are based on this common framework.
Note: in the following sections, I'm going to talk about JSX. However, I am not proposing that JSX support be added to Bevy.
A typical gizmo in my three.js system looks like this:
/** Component which displays aspects of fixtures which are normally invisible. */
export const FixtureOutlinesOverlay: VoidComponent<Props> = props => {
const toolState = useToolState();
const fixtures = createMemo(() => props.structure?.instances.list().filter(isFixture) ?? []);
const loader = new TextureLoader();
const mapPin = loader.load(mapPinImg);
const spriteColor = (config: IRegionAppearance, selected: boolean) =>
colord(config.color ?? '#ffdd88')
.darken(selected ? -0.2 : 0.2)
.toHex() as ColorRepresentation;
return (
<SceneProvider>
<For each={fixtures()}>
{fix => (
<>
<Show when={fix.aspectConfig(waymark)} keyed>
{config => (
<>
<FixtureOrientationOverlay
fixture={fix}
config={config}
selected={fix === toolState.selectedInstance}
/>
<Sprite
texture={mapPin}
location={() => fix.position}
scale={[0.25, 0.4, 0.25]}
color={spriteColor(config, fix === toolState.selectedInstance)}
center={mapPinCenter}
opacity={1}
/>
</>
)}
</Show>
<Show when={fix.aspectConfig(circleMarker)} keyed>
{config => (
<>
<FixtureOrientationOverlay
fixture={fix}
config={config}
selected={fix === toolState.selectedInstance}
/>
<FlatCircle
location={() => fix.position}
radius={fix.ensureProperties<ICircularRegionProps>().radius}
color={config.color as ColorRepresentation}
opacity={fix === toolState.selectedInstance ? 1 : 0.2}
/>
<Sprite
texture={mapPin}
location={() => fix.position}
scale={[0.25, 0.4, 0.25]}
color={spriteColor(config, fix === toolState.selectedInstance)}
center={mapPinCenter}
opacity={1}
/>
</>
)}
</Show>
<Show when={fix.ensureProperties<IWaypointsProps>().waypoints.length > 1}>
<DashedPolyLine
vertices={fix
.ensureProperties<IWaypointsProps>()
.waypoints.map(wp => wp.position)}
dashLength={0.2}
lineWidth={0.09}
opacity={fix === toolState.selectedInstance ? 0.7 : 0.3}
occlusionFactor={0.6}
/>
</Show>
</Show>
</>
)}
</For>
</SceneProvider>
);
};
And here's a screenshot of what that looks like:
Things I want to point out about this code:
- Gizmos are composed of multiple translucent primitives.
- Primitives can include meshes, lines, sprites and floating text.
- Gizmos can also contain reusable "components" such as FlatCircle and DashedPolyline, which are themselves Gizmos.
- Most primitives are 2D meshes like circles and rectangles.
- The attributes of the primitives are reactive: that is, when the game state changes, the Gizmo vertex buffers and material properties are automatically re-calculated. So for example in the above code, the line
opacity={fix === toolState.selectedInstance ? 1 : 0.2}
means that the opacity of the selected fixture is higher than fixtures that are not selected. - In my TypeScript code, I use JSX syntax to declaratively define Gizmos. The system is similar in concept to react-three-fiber, however this system is based on Solid.js rather than React.
- Iteration and Conditions are supported via the standard Solid.js components
For
andShow
. So if I want to display a bounding box for every physics collider, it's a simple matter to iterate over the list of colliders.
For implementing 2D primitives such as circles and rectangles, I have a 2D drawing library which generates meshes. So for example, the DashedPolyLine
component calls drawShapes.strokePolyLine()
, which fills in a vertex buffer and index buffer. This library also accepts a transform, so for example the Gizmo that draws portal apertures can align the generated rectangle with the portal.
Now, I know that Bevy is not a reactive framework like Leptos. However, it does have the ability to trigger behaviors on change events. So something like what I am describing is not entirely unfeasible, although it would no doubt look very different than the code that I have shown.