jos_valentin
jos_valentin2w ago

Taking the first player that triggers an action

Which would be a good approach for this case? I have some players in a table and I want to give a point for the one that press the button in the table first (when the lights turn on for example), Should I have a button owner that listen to the players trigger (ej a realtime property boolean for each player model or even an rpc call) or I can use a realtime collection in a model that has no owner to manage it? (since it is suppose that collections are prepared to avoid race conditions)
13 Replies
kapa.ai
kapa.ai2w ago
Hi @jos_valentin. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave: ---------------------- For your scenario of determining which player presses a button first, I'd recommend using a RealtimeModel with ownership to manage this interaction. Here's how you could approach it: Using a RealtimeModel with Ownership You could create a custom RealtimeModel for your button that tracks which player pressed it first. The key is to use Normcore's ownership system to handle the race condition.
[RealtimeModel(createMetaModel: true)]
public partial class ButtonModel {
[RealtimeProperty(1, true)]
private int _firstPlayerID;

[RealtimeProperty(2, true)]
private bool _isPressed;

[RealtimeProperty(3, true)]
private bool _isActive; // For when lights are on
}
[RealtimeModel(createMetaModel: true)]
public partial class ButtonModel {
[RealtimeProperty(1, true)]
private int _firstPlayerID;

[RealtimeProperty(2, true)]
private bool _isPressed;

[RealtimeProperty(3, true)]
private bool _isActive; // For when lights are on
}
Then create a RealtimeComponent that uses this model:
public class ButtonController : RealtimeComponent<ButtonModel> {
// When we connect to the room, we'll get a model to work with
protected override void OnRealtimeModelReplaced(ButtonModel previousModel, ButtonModel currentModel) {
if (previousModel != null) {
// Unsubscribe from previous model
}

if (currentModel != null) {
// Subscribe to new model
}
}

// Called when a player tries to press the button
public void TryPressButton(int playerID) {
// Only allow pressing when the button is active (lights on)
if (model.isActive && !model.isPressed) {
// Request ownership to be the one who decides
RequestOwnership();

// If we got ownership, we can set the first player
if (isOwnedLocallySelf) {
model.firstPlayerID = playerID;
model.isPressed = true;
}
}
}

// Method to activate the button (when lights turn on)
public void ActivateButton() {
RequestOwnership();
if (isOwnedLocallySelf) {
model.isActive = true;
model.isPressed = false;
model.firstPlayerID = -1; // Reset
}
}
}
public class ButtonController : RealtimeComponent<ButtonModel> {
// When we connect to the room, we'll get a model to work with
protected override void OnRealtimeModelReplaced(ButtonModel previousModel, ButtonModel currentModel) {
if (previousModel != null) {
// Unsubscribe from previous model
}

if (currentModel != null) {
// Subscribe to new model
}
}

// Called when a player tries to press the button
public void TryPressButton(int playerID) {
// Only allow pressing when the button is active (lights on)
if (model.isActive && !model.isPressed) {
// Request ownership to be the one who decides
RequestOwnership();

// If we got ownership, we can set the first player
if (isOwnedLocallySelf) {
model.firstPlayerID = playerID;
model.isPressed = true;
}
}
}

// Method to activate the button (when lights turn on)
public void ActivateButton() {
RequestOwnership();
if (isOwnedLocallySelf) {
model.isActive = true;
model.isPressed = false;
model.firstPlayerID = -1; // Reset
}
}
}
This approach leverages Normcore's ownership system to handle the race condition. When multiple players try to press the button simultaneously, only one will successfully gain ownership and set the firstPlayerID property Requesting ownership of a RealtimeComponent. Why This Approach Works Better Than Alternatives 1. Avoiding RPCs: Using RPCs for this would be problematic as mentioned in the documentation: "RPC messages in Normcore are available as a last resort. If you have players that can join a room late, RPC messages make consistent synchronization difficult" Room RPCs. 2. Race Condition Handling: The ownership system ensures that only one client can update the model at a time, preventing race conditions Ownership and Lifetime Flags. 3. Server Authority: This approach uses Normcore's server authority to determine which client's request is processed first, making it fair for all players. The key insight is that by using RequestOwnership(), you're letting Normcore's server decide which client gets to update the model first, which is exactly what you need for a "first to press" mechanic. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai2w ago
<@391444583034257420> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
Mechabit
Mechabit2w ago
you can sync a list of server times which will be the same for all players regardless of ping time you'd also want to turn on the light at a future server time so it appears at the same time for all players so the owner doesn't get a head start
jos_valentin
jos_valentinOP2w ago
Timers + Synchronized Playback | Normcore Documenation
A common need in Normcore is to synchronize a timeline across all clients for music playback or animation playback. A naive approach would be to choose a main client and have it send updates to all clients to update their timelines. This uses a lot of bandwidth, it can result in choppy playback for the other clients, and playback isn't guarantee...
Mechabit
Mechabit2w ago
that's a good start
jos_valentin
jos_valentinOP2w ago
and how could I handle which player touch the button first? any of the options I mencioned would be good or the one that the bot said maybe?
Mechabit
Mechabit2w ago
if you want to take ping out of the equation you need to store a list of server times when players hit their buttons @kapa.ai how do I sync a list of server times when players press a button
kapa.ai
kapa.ai2w ago
Let me check the knowledge base for you again. One moment! :books: ---------------------- To sync a list of server times when players press a button in Normcore, you'll need to use a RealtimeModel with a collection to store the timestamps. Here's how you can implement this: Using RealtimeSet to Store Timestamps Normcore provides collections like StringKeyDictionary and RealtimeSet that are designed to handle concurrent updates properly. For your use case, you can create a model that stores timestamps when players press a button:
using Normal.Realtime;
using Normal.Realtime.Serialization;
using System;

[RealtimeModel]
public partial class ButtonPressModel {
[RealtimeProperty(1, true)]
private RealtimeSet<ButtonPressEvent> _buttonPresses;
}

[RealtimeModel]
public partial class ButtonPressEvent {
[RealtimeProperty(1, true)]
private int _playerID;

[RealtimeProperty(2, true)]
private double _serverTime;
}
using Normal.Realtime;
using Normal.Realtime.Serialization;
using System;

[RealtimeModel]
public partial class ButtonPressModel {
[RealtimeProperty(1, true)]
private RealtimeSet<ButtonPressEvent> _buttonPresses;
}

[RealtimeModel]
public partial class ButtonPressEvent {
[RealtimeProperty(1, true)]
private int _playerID;

[RealtimeProperty(2, true)]
private double _serverTime;
}
Then create a component to manage these button presses:
public class ButtonPressManager : RealtimeComponent<ButtonPressModel> {
protected override void OnRealtimeModelReplaced(ButtonPressModel previousModel, ButtonPressModel currentModel) {
if (previousModel != null) {
// Unsubscribe from previous model events if needed
}

if (currentModel != null) {
if (currentModel.buttonPresses == null) {
currentModel.buttonPresses = new RealtimeSet<ButtonPressEvent>();
}
}
}

public void RecordButtonPress(int playerID) {
// Create a new button press event
ButtonPressEvent pressEvent = new ButtonPressEvent();
pressEvent.playerID = playerID;

// Use the room's server time
pressEvent.serverTime = realtime.room.time;

// Add to the collection
model.buttonPresses.Add(pressEvent);
}

// Get all button presses sorted by time
public ButtonPressEvent[] GetButtonPressesByTime() {
if (model == null || model.buttonPresses == null)
return new ButtonPressEvent[0];

ButtonPressEvent[] presses = model.buttonPresses.ToArray();
Array.Sort(presses, (a, b) => a.serverTime.CompareTo(b.serverTime));
return presses;
}
}
public class ButtonPressManager : RealtimeComponent<ButtonPressModel> {
protected override void OnRealtimeModelReplaced(ButtonPressModel previousModel, ButtonPressModel currentModel) {
if (previousModel != null) {
// Unsubscribe from previous model events if needed
}

if (currentModel != null) {
if (currentModel.buttonPresses == null) {
currentModel.buttonPresses = new RealtimeSet<ButtonPressEvent>();
}
}
}

public void RecordButtonPress(int playerID) {
// Create a new button press event
ButtonPressEvent pressEvent = new ButtonPressEvent();
pressEvent.playerID = playerID;

// Use the room's server time
pressEvent.serverTime = realtime.room.time;

// Add to the collection
model.buttonPresses.Add(pressEvent);
}

// Get all button presses sorted by time
public ButtonPressEvent[] GetButtonPressesByTime() {
if (model == null || model.buttonPresses == null)
return new ButtonPressEvent[0];

ButtonPressEvent[] presses = model.buttonPresses.ToArray();
Array.Sort(presses, (a, b) => a.serverTime.CompareTo(b.serverTime));
return presses;
}
}
Why This Approach Works 1. Server Time Synchronization: Using realtime.room.time gives you access to the synchronized server clock, which ensures all timestamps are based on the same reference point Timers + Synchronized Playback. 2. Race Condition Handling: The RealtimeSet collection is designed to handle concurrent updates from multiple clients, making it ideal for recording button presses that might happen simultaneously Room Common Questions. 3. Automatic Synchronization: Normcore's datastore automatically synchronizes all changes to the collection across all clients, so everyone will see the same list of button presses in the same order. This approach gives you a chronologically ordered list of button presses with accurate server timestamps, allowing you to determine which player pressed the button first or analyze the timing of all button presses. For more complex scenarios, you might want to add additional properties to the ButtonPressEvent model, such as the specific button that was pressed or other contextual information. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai2w ago
<@162940911964258305> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
Mechabit
Mechabit2w ago
you also want the light to turn on at the same time regaurdless of ping @kapa.ai how do I turn on a light at a future server time
kapa.ai
kapa.ai2w ago
Let me check the knowledge base for you again. One moment! :books: ---------------------- To turn on a light at a future server time in Normcore, you can use the synchronized room time to schedule events. Here's how you can implement this functionality: Using Synchronized Server Time for Scheduled Events You can create a RealtimeModel that stores the scheduled time for the light to turn on, and then check against the current server time in your Update method:
using Normal.Realtime;
using UnityEngine;

[RealtimeModel]
public partial class LightSchedulerModel {
[RealtimeProperty(1, true)]
private double _scheduledOnTime;
}

public class LightScheduler : RealtimeComponent<LightSchedulerModel> {
[SerializeField] private Light _light;
private bool _lightIsOn = false;

private void Update() {
if (model == null || _light == null)
return;

// Check if it's time to turn on the light
if (!_lightIsOn && realtime.room.time >= model.scheduledOnTime) {
_light.enabled = true;
_lightIsOn = true;
}
}

// Schedule the light to turn on at a future time (in seconds from now)
public void ScheduleLightOn(float secondsFromNow) {
if (model == null)
return;

// Calculate the future server time
double futureTime = realtime.room.time + secondsFromNow;

// Request ownership to update the model
RequestOwnership();
if (isOwnedLocallySelf) {
model.scheduledOnTime = futureTime;
_lightIsOn = false;
_light.enabled = false;
}
}

// Reset the light (for testing)
public void ResetLight() {
_lightIsOn = false;
_light.enabled = false;
}
}
using Normal.Realtime;
using UnityEngine;

[RealtimeModel]
public partial class LightSchedulerModel {
[RealtimeProperty(1, true)]
private double _scheduledOnTime;
}

public class LightScheduler : RealtimeComponent<LightSchedulerModel> {
[SerializeField] private Light _light;
private bool _lightIsOn = false;

private void Update() {
if (model == null || _light == null)
return;

// Check if it's time to turn on the light
if (!_lightIsOn && realtime.room.time >= model.scheduledOnTime) {
_light.enabled = true;
_lightIsOn = true;
}
}

// Schedule the light to turn on at a future time (in seconds from now)
public void ScheduleLightOn(float secondsFromNow) {
if (model == null)
return;

// Calculate the future server time
double futureTime = realtime.room.time + secondsFromNow;

// Request ownership to update the model
RequestOwnership();
if (isOwnedLocallySelf) {
model.scheduledOnTime = futureTime;
_lightIsOn = false;
_light.enabled = false;
}
}

// Reset the light (for testing)
public void ResetLight() {
_lightIsOn = false;
_light.enabled = false;
}
}
This approach leverages Normcore's synchronized room time, which is available via the realtime.room.time property. This time is synchronized across all clients to match the room server's clock exactly with no added latency, as mentioned in the Timers + Synchronized Playback documentation. Why This Works Well 1. Synchronized Timing: All clients will turn on the light at the same server time, ensuring the action appears synchronized to all players. 2. No Continuous Updates Needed: Once the scheduled time is set, you don't need to send any additional network messages - each client independently checks against the server time. 3. Late-Join Support: If a player joins after the scheduling but before the light turns on, they'll still see the light turn on at the correct time because the scheduled time is stored in the model. You can extend this concept to create more complex timed events by adding additional properties to your model, such as a duration for how long the light stays on, or by using collections to schedule multiple events. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai2w ago
<@162940911964258305> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
jos_valentin
jos_valentinOP2w ago
Thanks for the resources, I'll check it out 🙂

Did you find this page helpful?