Adding Custom UI Elements with React
oncyber uses the React (opens in a new tab) library as the primary approach to creating a custom in-world UI for your experience.
Here's how you can render a simple React component in a Behavior:
import { ScriptBehavior, UI } from '@oo/scripting'
const style = {
position: "fixed",
backgroundColor: "white",
top: "20px",
left: "50%",
transform: "translateX(-50%)",
}
function UiElement() {
return <div style={style}>
Hello, World
</div>
}
export default class ExampleBehavior extends ScriptBehavior {
private renderer = UI.createRenderer();
onReady = () => {
this.renderer.render(<UiElement/>);
}
onDispose = () => {
this.renderer.unmount();
}
}
This method can be used in other types of scripts as well -- not just Behaviors.
Although a renderer can only display a single React component, you can create additional UI elements with React by creating multiple renderers.
Creating a Reactive UI
In the prior example, our UI element is static. Although this is fine for UI elements intended as a permanent fixture (ie. a graphic HUD frame/container for other UI elements), anything variable such as a score or player health will need to be made dynamic.
Creating a dynamic (or "Reactive") UI in oncyber can be approached in several ways -- next, we'll take a look at a few examples.
Manual Rerendering
One approach is to manually specify a re-render for the UI each time we change a variable:
import { ScriptBehavior, Param, UI } from '@oo/scripting'
export default class ExampleBehavior extends ScriptBehavior {
private renderer = UI.createRenderer();
private count = 0;
onReady = () => {
this.render();
}
render = () => {
this.renderer.render(<this.ui/>);
};
action = () => {
this.count++;
this.render();
}
ui = () => {
return <div
style={{
position: "fixed",
backgroundColor: "white",
top: "20px",
left: "50%",
transform: "translateX(-50%)",
}}
>
<div> Count: {this.count} </div>
<button
onClick={this.action}
>
Button
</button>
</div>
}
onDispose = () => {
this.renderer.unmount();
}
}
Automatic Re-Rendering
If you prefer to lean more into React's style, you can use a Store.
This allows us to automate UI re-rendering by creating a Store that keeps track of some values we want displayed in our UI.
In the example that follows, we'll create a Store that keeps track of our count, add a way to increment it, and have our component update when that value changes:
import { ScriptBehavior, UI, Store, useStore } from '@oo/scripting'
const store = new Store({
count: 0,
});
function Button() {
const { count } = useStore(store);
function increaseCount() {
store.update({ count: count+1 });
}
return <div
style={{
position: "fixed",
backgroundColor: "white",
top: "20px",
left: "50%",
transform: "translateX(-50%)",
}}
>
<div> Count: {count} </div>
<button
onClick={increaseCount}
>
Button
</button>
</div>
}
export default class ExampleBehavior extends ScriptBehavior {
private renderer = UI.createRenderer();
get count() {
return store.state.count;
}
onReady = () => {
this.renderer.render(<Button/>)
}
onEnd = () => {
console.log("current count:", this.count)
}
onDispose = () => {
this.renderer.unmount()
}
}
For further modularity, you can also isolate and export the Store for use in your other scripts.
Let's say you have two scripts, A and B.
In A, you create your Store:
import { Store } from '@oo/scripting'
export const store = new Store({
score: 0,
level: 0,
});
Next, in Script B, you import the Store from Script A:
import { store } from "./A"
// read the score from the store
const score = store.state.score;
// change the state of the store
store.update({ score: score+1 });
// subscribe to updates
const unsubscribe = store.subscribe(() => {
console.log("store has been updated");
})
// ...
unsubscribe();
This allows you to take a more modular approach with your Store, making it independently accessible instead of tying it to another script's code.
Pointer Events
It's important to disable pointer events for on-screen UI elements that are not interactive. UI elements with pointer events will prevent users from locking the mouse when clicking on a them.
Disabling pointer events for UI elements you've created is simple -- when defining style, just add a line that sets pointerEvents
to none
:
function Score() {
const { score } = useStore(store);
return <div
style={{
pointerEvents: "none",
position: "fixed",
backgroundColor: "white",
fontSize: "20px"
top: "20px",
left: "50%",
transform: "translateX(-50%)",
}}
>
Score: {score}
</div>
}