In general, your UI code should depend on your core game logic. But your core game logic should not even be aware of the existence of your UI code. In other words, your JS code will be calling stuff from your C# code, but never the other way around. This one-directional dependency makes everything easy to maintain. You can modify the UI as much as you like without risking any unintended side effects in your game logic.
The best way to implement this is via C# events (or similar pub/sub mechanisms). Whenever your UI needs something, you can have your core game logic fire an event. And in your JS code, you can subscribe to C# events by appending “add_” and “remove_” to the event name.
Here’s a quick example:
// You can add this MonoBehaviour as 'spawner' in ScriptEngine's Globals list.
public class TreasureChestSpawner : MonoBehaviour {
// Fired when a chest is spawned in the scene
public event Action OnChestSpawned;
...
}
// `spawner` will now be available in your JS code as a global variable.
spawner.add_OnChestSpawned(onChestSpawned)
function onChestSpawned() {
log("yay!")
}
// Event handler can be removed via `spawner.remove_OnChestSpawned(onChestSpawned)`
// Optional TS Definition
declare namespace CS {
namespace MyGame {
export interface ChestSpawner {
add_OnChestSpawned(handler: Function): void
remove_OnChestSpawned(handler: Function): void
}
}
}
declare const spawner: CS.MyGame.ChestSpawner;
Reducing Boilerplates
C# events need to be properly cleaned up from the JS/Preact side. Compound that with the “add_” and “remove_” event syntax, you usually end up with a bit of verbose boilerplate. This is where you can make use of OneJS’s useEventfulState()
function to reduce the following boilerplate:
// Assuming you've added OneJS.Samples.SampleCharacter as 'sam' to the Globals list
const App = () => {
const [health, setHealth] = useState(sam.Health)
const [maxHealth, setMaxHealth] = useState(sam.MaxHealth)
useEffect(() => {
sam.add_OnHealthChanged(onHealthChanged)
sam.add_OnMaxHealthChanged(onMaxHealthChanged)
onejs.subscribe("onReload", () => {
// Cleaning up for Live Reload
sam.remove_OnHealthChanged(onHealthChanged)
sam.remove_OnMaxHealthChanged(onMaxHealthChanged)
})
return () => {
sam.remove_OnHealthChanged(onHealthChanged)
sam.remove_OnMaxHealthChanged(onMaxHealthChanged)
}
}, [])
function onHealthChanged(v: number): void {
setHealth(v)
}
function onMaxHealthChanged(v: number): void {
setMaxHealth(v)
}
return <div>...</div>
}
To just:
const App = () => {
const [health, setHealth] = useEventfulState(sam, "Health")
const [maxHealth, setMaxHealth] = useEventfulState(sam, "MaxHealth")
return <div>...</div>
}
useEventfulState()
will take care of the event subscription and cleanup for you automatically.
NOTE:
useEventfulState(obj, "Health")
assumes the C#obj
has a property named “Health
” and an event named “OnHealthChanged
” (both of which can also be auto-generated by the Source Generator below).
C# Source Generator
You may also use the EventfulProperty attribute to further reduce boilerplates on the C# side and turn this:
public class Character : MonoBehaviour {
public float Health {
get { return _health; }
set {
_health = value;
OnHealthChanged?.Invoke(_health);
}
}
public event Action<float> OnHealthChanged;
public float MaxHealth {
get { return _maxHealth; }
set {
_maxHealth = value;
OnMaxHealthChanged?.Invoke(_maxHealth);
}
}
public event Action<float> OnMaxHealthChanged;
float _health = 200f;
float _maxHealth = 200f;
}
Into just this:
public partial class Character : MonoBehaviour {
[EventfulProperty] float _health = 200f;
[EventfulProperty] float _maxHealth = 200f;
}
Note the partial
keyword being used on the class declaration. The corresponding getters, setters, and events will be auto-created by OneJS’s Source Generators .
onejs.subscribe
OneJS provides a handy onejs.subscribe()
function that allows you to subscribe to C# events from your JS code. A key benefit of this function is that it automatically handles event cleanup during LiveReload, helping you avoid memory leaks or orphaned event handlers.
Usage is like this:
const unsubscribe = onejs.subscribe("onReload", () => {
console.log("Engine Reloaded!")
})
const unsubscribe = onejs.subscribe(sam, "OnHealthChanged", () => {
console.log("Sample Character's health changed!")
})