Skip to content

Modular, Reactive Gizmos #9498

@viridia

Description

@viridia

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:

use-marks

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 and Show. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-GizmosVisual editor and debug gizmosC-FeatureA new feature, making something new possible

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions