SoundGuy2
SoundGuy29mo ago

Button press on one client

What is the right way to sync a button press form one client to the rest, should I use an RPC event?
Solution:
you should give up on the rpc recipe, instead sync the the menu state as an int and use the model changed event to update the menu on clients
Jump to solution
27 Replies
kapa.ai
kapa.ai9mo ago
Hi @SoundGuy2. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave: ---------------------- While RPC messages are available in Normcore, they are generally discouraged for use in most cases due to the difficulty they present in maintaining consistent synchronization across clients. Any state that is modified in response to an RPC message will need to be manually synchronized by your own code, which can lead to desyncs and bugs that are hard to reproduce and debug (source (https://normcore.io/documentation/guides/recipes/rpc-events#preface)). Instead, Normcore recommends using the room's datastore for synchronizing state across clients. When you make a change to the datastore, it is automatically replicated to all connected clients. This means that if you need to sync a button press from one client to the rest, you should change its state in the datastore, and the datastore will automatically notify all clients connected to the room so they can update their world to match (source (https://normcore.io/documentation/architecture/client#datastore)). However, if you absolutely need to use an RPC-like structure, Normcore provides a recipe for creating an RPC-like event message that can be fired by anyone in the room. This recipe involves creating a model that holds all the values you'd typically send in an RPC message and an integer that you can increment every time you want to trigger an event. You can then create a RealtimeComponent that uses this model to call a method on the button press whenever the event fires (source (https://normcore.io/documentation/guides/recipes/rpc-events#recipe)). Here's an example of how you can create such a model and RealtimeComponent:
[RealtimeModel]
public class ButtonPressEventModel {
[RealtimeProperty(1, true)] private int _trigger;
[RealtimeProperty(2, true)] private int _senderID;
// Used to fire an event on all clients
public void FireEvent(int senderID) {
this.trigger++;
this.senderID = senderID;
}

// An event that consumers of this model can subscribe to in order to respond to the event
public delegate void EventHandler(int senderID);
public event EventHandler eventDidFire;

// A RealtimeCallback method that fires whenever we read any values from the server
[RealtimeCallback(RealtimeModelEvent.OnDidRead)]
private void DidRead() {
if (eventDidFire != null && trigger != 0)
eventDidFire(senderID);
}
}

public class ButtonPressEvent : RealtimeComponent<ButtonPressEventModel> {
// When we connect to a room server, we'll be given an instance of our model to work with.
protected override void OnRealtimeModelReplaced(ButtonPressEventModel previousModel, ButtonPressEventModel currentModel) {
if (previousModel != null) {
// Unsubscribe from events on the old model.
previousModel.eventDidFire -= EventDidFire;
}
if (currentModel != null) {
// Subscribe to events on the new model
currentModel.eventDidFire += EventDidFire;
}
}

// A public method we can use to fire the event
public void Press() {
model.FireEvent(realtime.clientID);
}

// Called whenever our event fires
private void EventDidFire(int senderID) {
// Do something in response to the button press event
}
}
[RealtimeModel]
public class ButtonPressEventModel {
[RealtimeProperty(1, true)] private int _trigger;
[RealtimeProperty(2, true)] private int _senderID;
// Used to fire an event on all clients
public void FireEvent(int senderID) {
this.trigger++;
this.senderID = senderID;
}

// An event that consumers of this model can subscribe to in order to respond to the event
public delegate void EventHandler(int senderID);
public event EventHandler eventDidFire;

// A RealtimeCallback method that fires whenever we read any values from the server
[RealtimeCallback(RealtimeModelEvent.OnDidRead)]
private void DidRead() {
if (eventDidFire != null && trigger != 0)
eventDidFire(senderID);
}
}

public class ButtonPressEvent : RealtimeComponent<ButtonPressEventModel> {
// When we connect to a room server, we'll be given an instance of our model to work with.
protected override void OnRealtimeModelReplaced(ButtonPressEventModel previousModel, ButtonPressEventModel currentModel) {
if (previousModel != null) {
// Unsubscribe from events on the old model.
previousModel.eventDidFire -= EventDidFire;
}
if (currentModel != null) {
// Subscribe to events on the new model
currentModel.eventDidFire += EventDidFire;
}
}

// A public method we can use to fire the event
public void Press() {
model.FireEvent(realtime.clientID);
}

// Called whenever our event fires
private void EventDidFire(int senderID) {
// Do something in response to the button press event
}
}
Please note that this is a simplified example and you may need to adjust it to fit your specific needs. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai9mo ago
<@1081953198014734357> kapa.ai is still learning and improving, please let us know how it did by reacting below
SoundGuy2
SoundGuy2OP9mo ago
@Normcore Assistant can you give me the following core - I am pressing a button with an int number in one client a function is called in another client with that number in the other clients
kapa.ai
kapa.ai9mo ago
Let me check the knowledge base for you. One moment! :books: ---------------------- Sure, you can achieve this by using Normcore's RealtimeModel and RealtimeComponent. Here's an example of how you can create a model and a RealtimeComponent for a button press event that includes an integer value:
[RealtimeModel]
public partial class ButtonPressEventModel {
[RealtimeProperty(1, true)] private int _trigger;
[RealtimeProperty(2, true)] private int _senderID;
[RealtimeProperty(3, true)] private int _buttonNumber;

// Used to fire an event on all clients
public void FireEvent(int senderID, int buttonNumber) {
this.trigger++;
this.senderID = senderID;
this.buttonNumber = buttonNumber;
}

// An event that consumers of this model can subscribe to in order to respond to the event
public delegate void EventHandler(int senderID, int buttonNumber);
public event EventHandler eventDidFire;

// A RealtimeCallback method that fires whenever we read any values from the server
[RealtimeCallback(RealtimeModelEvent.OnDidRead)]
private void DidRead() {
if (eventDidFire != null && trigger != 0)
eventDidFire(senderID, buttonNumber);
}
}

public class ButtonPressEvent : RealtimeComponent<ButtonPressEventModel> {
// When we connect to a room server, we'll be given an instance of our model to work with.
protected override void OnRealtimeModelReplaced(ButtonPressEventModel previousModel, ButtonPressEventModel currentModel) {
if (previousModel != null) {
// Unsubscribe from events on the old model.
previousModel.eventDidFire -= EventDidFire;
}
if (currentModel != null) {
// Subscribe to events on the new model
currentModel.eventDidFire += EventDidFire;
}
}

// A public method we can use to fire the event
public void Press(int buttonNumber) {
model.FireEvent(realtime.clientID, buttonNumber);
}
// Called whenever our event fires
private void EventDidFire(int senderID, int buttonNumber) {
// Do something in response to the button press event
}
}
[RealtimeModel]
public partial class ButtonPressEventModel {
[RealtimeProperty(1, true)] private int _trigger;
[RealtimeProperty(2, true)] private int _senderID;
[RealtimeProperty(3, true)] private int _buttonNumber;

// Used to fire an event on all clients
public void FireEvent(int senderID, int buttonNumber) {
this.trigger++;
this.senderID = senderID;
this.buttonNumber = buttonNumber;
}

// An event that consumers of this model can subscribe to in order to respond to the event
public delegate void EventHandler(int senderID, int buttonNumber);
public event EventHandler eventDidFire;

// A RealtimeCallback method that fires whenever we read any values from the server
[RealtimeCallback(RealtimeModelEvent.OnDidRead)]
private void DidRead() {
if (eventDidFire != null && trigger != 0)
eventDidFire(senderID, buttonNumber);
}
}

public class ButtonPressEvent : RealtimeComponent<ButtonPressEventModel> {
// When we connect to a room server, we'll be given an instance of our model to work with.
protected override void OnRealtimeModelReplaced(ButtonPressEventModel previousModel, ButtonPressEventModel currentModel) {
if (previousModel != null) {
// Unsubscribe from events on the old model.
previousModel.eventDidFire -= EventDidFire;
}
if (currentModel != null) {
// Subscribe to events on the new model
currentModel.eventDidFire += EventDidFire;
}
}

// A public method we can use to fire the event
public void Press(int buttonNumber) {
model.FireEvent(realtime.clientID, buttonNumber);
}
// Called whenever our event fires
private void EventDidFire(int senderID, int buttonNumber) {
// Do something in response to the button press event
}
}
In this example, when a button is pressed on one client, the Press method is called with the button number as an argument. This fires an event on all clients, triggering the EventDidFire method on each client with the sender's ID and the button number as arguments. You can replace the comment in the EventDidFire method with the function you want to call on the other clients. Please note that this is a simplified example and you may need to adjust it to fit your specific needs. You can find more information about creating a RealtimeModel and RealtimeComponent in the Normcore documentation (https://normcore.io/documentation/guides/recipes/rpc-events#recipe). ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai9mo ago
<@1081953198014734357> kapa.ai is still learning and improving, please let us know how it did by reacting below
SoundGuy2
SoundGuy2OP9mo ago
@Tom why isn't this code nice like in the tickets you opened?
Towellie
Towellie9mo ago
I was just looking at that myself discord has character limits and it seems the length spanned multiple messages
[RealtimeModel]
public partial class ButtonPressEventModel {
[RealtimeProperty(1, true)] private int _trigger;
[RealtimeProperty(2, true)] private int _senderID;
[RealtimeProperty(3, true)] private int _buttonNumber;

// Used to fire an event on all clients
public void FireEvent(int senderID, int buttonNumber) {
this.trigger++;
this.senderID = senderID;
this.buttonNumber = buttonNumber;
}

// An event that consumers of this model can subscribe to in order to respond to the event
public delegate void EventHandler(int senderID, int buttonNumber);
public event EventHandler eventDidFire;

// A RealtimeCallback method that fires whenever we read any values from the server
[RealtimeCallback(RealtimeModelEvent.OnDidRead)]
private void DidRead() {
if (eventDidFire != null && trigger != 0)
eventDidFire(senderID, buttonNumber);
}
}

public class ButtonPressEvent : RealtimeComponent<ButtonPressEventModel> {
// When we connect to a room server, we'll be given an instance of our model to work with.
protected override void OnRealtimeModelReplaced(ButtonPressEventModel previousModel, ButtonPressEventModel currentModel) {
if (previousModel != null) {
// Unsubscribe from events on the old model.
previousModel.eventDidFire -= EventDidFire;
}
if (currentModel != null) {
// Subscribe to events on the new model
currentModel.eventDidFire += EventDidFire;
}
}

// A public method we can use to fire the event
public void Press(int buttonNumber) {
model.FireEvent(realtime.clientID, buttonNumber);
}
// Called whenever our event fires
private void EventDidFire(int senderID, int buttonNumber) {
// Do something in response to the button press event
}
}
[RealtimeModel]
public partial class ButtonPressEventModel {
[RealtimeProperty(1, true)] private int _trigger;
[RealtimeProperty(2, true)] private int _senderID;
[RealtimeProperty(3, true)] private int _buttonNumber;

// Used to fire an event on all clients
public void FireEvent(int senderID, int buttonNumber) {
this.trigger++;
this.senderID = senderID;
this.buttonNumber = buttonNumber;
}

// An event that consumers of this model can subscribe to in order to respond to the event
public delegate void EventHandler(int senderID, int buttonNumber);
public event EventHandler eventDidFire;

// A RealtimeCallback method that fires whenever we read any values from the server
[RealtimeCallback(RealtimeModelEvent.OnDidRead)]
private void DidRead() {
if (eventDidFire != null && trigger != 0)
eventDidFire(senderID, buttonNumber);
}
}

public class ButtonPressEvent : RealtimeComponent<ButtonPressEventModel> {
// When we connect to a room server, we'll be given an instance of our model to work with.
protected override void OnRealtimeModelReplaced(ButtonPressEventModel previousModel, ButtonPressEventModel currentModel) {
if (previousModel != null) {
// Unsubscribe from events on the old model.
previousModel.eventDidFire -= EventDidFire;
}
if (currentModel != null) {
// Subscribe to events on the new model
currentModel.eventDidFire += EventDidFire;
}
}

// A public method we can use to fire the event
public void Press(int buttonNumber) {
model.FireEvent(realtime.clientID, buttonNumber);
}
// Called whenever our event fires
private void EventDidFire(int senderID, int buttonNumber) {
// Do something in response to the button press event
}
}
here you go
SoundGuy2
SoundGuy2OP9mo ago
@Normcore Assistant does you last example use RPC or not?
kapa.ai
kapa.ai9mo ago
Let me check the knowledge base for you. One moment! :books: ---------------------- The example provided does not use RPC in the traditional sense. Instead, it uses Normcore's RealtimeModel and RealtimeComponent to achieve a similar effect. In this example, the button press event is represented as a change in the state of a RealtimeModel. When the button is pressed, the FireEvent method is called, which increments the _trigger property and sets the _senderID and _buttonNumber properties. This change in the model's state is automatically synchronized across all clients connected to the room. When a client receives the updated model from the server, it triggers the DidRead method, which in turn fires the eventDidFire event. This event is handled by the EventDidFire method in the RealtimeComponent, which can then call any function you want with the sender's ID and the button number as arguments. This approach provides a way to send messages between clients that is similar to RPC, but it avoids some of the synchronization issues that can occur with traditional RPC messages (source (https://normcore.io/documentation/guides/recipes/rpc-events#preface)). ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai9mo ago
<@1081953198014734357> kapa.ai is still learning and improving, please let us know how it did by reacting below
SoundGuy2
SoundGuy2OP9mo ago
ok thanks @Normcore Assistant
kapa.ai
kapa.ai9mo ago
Let me check the knowledge base for you. One moment! :books: ---------------------- You're welcome! If you have any more questions about Normcore or VR development, feel free to ask. Happy coding! ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai9mo ago
<@1081953198014734357> kapa.ai is still learning and improving, please let us know how it did by reacting below
SoundGuy2
SoundGuy2OP9mo ago
@Normcore Assistant can the realtimeModel and component in the last example can be in the same Cs file or do i need to put the model in a separate cs file like in the other examples
kapa.ai
kapa.ai9mo ago
Let me check the knowledge base for you. One moment! :books: ---------------------- Yes, the RealtimeModel and the RealtimeComponent can be in the same C# file. They are two separate classes and can coexist in the same file without any issues. However, for organizational purposes and to maintain clean code, it's often recommended to keep them in separate files, especially if they start to get large or complex. But this is not a requirement, just a suggestion for best practices. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai9mo ago
<@1081953198014734357> kapa.ai is still learning and improving, please let us know how it did by reacting below
SoundGuy2
SoundGuy2OP9mo ago
@Normcore Assistant what about that .g file that is being auto regerated? @Normcore Assistant what about that .g file that is being auto regerated?
kapa.ai
kapa.ai9mo ago
Let me check the knowledge base for you. One moment! :books: ---------------------- The .g file you're referring to is likely the auto-generated code file that Normcore creates when you define a RealtimeModel. This file contains the code that Normcore uses to handle the synchronization of your model's state across all connected clients. If you're using Unity 2021 or newer, Normcore will automatically generate this code for you. You won't see this code in your file; it exists as temporary C# code that is generated when your project compiles (source (https://normcore.io/documentation/realtime/synchronizing-custom-data#generating-model-code-in-unity-2021)). If you're using an older version of Unity, you will need to manually compile the model in the Unity editor by highlighting the class and clicking "Compile Model." If any script in your project does not compile, Normcore's manual model compiler will be unable to load any of the state of your model (source (https://normcore.io/documentation/room/realtimemodel#compiling-a-model)). In either case, you don't need to worry about the contents of the .g file. Normcore handles all of the synchronization logic for you, so you can focus on defining the state you want to synchronize and how your application should respond to changes in that state. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai9mo ago
<@1081953198014734357> kapa.ai is still learning and improving, please let us know how it did by reacting below
SoundGuy2
SoundGuy2OP9mo ago
@Normcore Assistant the code above is faulty. I'm getting an endless calls to the EventDidFire function after pressing the button once
kapa.ai
kapa.ai9mo ago
Let me check the knowledge base for you. One moment! :books: ---------------------- The issue you're experiencing might be due to the DidRead method in the ButtonPressEventModel class. This method is called whenever any values are read from the server, and it fires the eventDidFire event if the trigger property is not zero. This could result in the EventDidFire method being called continuously if the trigger property is not reset to zero after the event is fired. To fix this, you can add a line of code to reset the trigger property to zero in the DidRead method after the eventDidFire event is fired. Here's how you can modify the DidRead method:
[RealtimeCallback(RealtimeModelEvent.OnDidRead)]
private void DidRead() {
if (eventDidFire != null && trigger != 0) {
eventDidFire(senderID, buttonNumber);
trigger = 0; // Reset the trigger property to zero
}
}
[RealtimeCallback(RealtimeModelEvent.OnDidRead)]
private void DidRead() {
if (eventDidFire != null && trigger != 0) {
eventDidFire(senderID, buttonNumber);
trigger = 0; // Reset the trigger property to zero
}
}
Please note that this is a simplified solution and you may need to adjust it to fit your specific needs. You can find more information about creating a RealtimeModel and RealtimeComponent in the Normcore documentation (https://normcore.io/documentation/guides/recipes/rpc-events#recipe). ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai9mo ago
<@1081953198014734357> kapa.ai is still learning and improving, please let us know how it did by reacting below
SoundGuy2
SoundGuy2OP9mo ago
@Normcore Assistant this is also not good. the event being only received in the remote client every once in a while . is it possible that the trigger changs to 0 in the local client before sendingg the button number ?
kapa.ai
kapa.ai9mo ago
Let me check the knowledge base for you. One moment! :books: ---------------------- Yes, it's possible that the trigger property is being reset to zero before the change is sent to the remote clients. This could happen if the local client reads the updated model from the server before the remote clients do. When the local client reads the updated model, it resets the trigger property to zero, which could prevent the remote clients from receiving the event. To fix this, you could add a delay before resetting the trigger property to zero. However, this might not be the best solution as it could lead to other synchronization issues. A better solution would be to use a different property to indicate that the button has been pressed. For example, you could add a buttonPressed property to the ButtonPressEventModel class and set this property to true when the button is pressed and to false when the event has been fired. Here's how you can modify the ButtonPressEventModel class:
[RealtimeModel]
public partial class ButtonPressEventModel {
[RealtimeProperty(1, true)] private int _trigger;
[RealtimeProperty(2, true)] private int _senderID;
[RealtimeProperty(3, true)] private int _buttonNumber;
[RealtimeProperty(5, true)] private bool _buttonPressed; // New property

// Used to fire an event on all clients
public void FireEvent(int senderID, int buttonNumber) {
this.trigger++;
this.senderID = senderID;
this.buttonNumber = buttonNumber;
this.buttonPressed = true; // Set the buttonPressed property to true
}

// An event that consumers of this model can subscribe to in order to respond to the event
public delegate void EventHandler(int senderID, int buttonNumber);
public event EventHandler eventDidFire;
// A RealtimeCallback method that fires whenever we read any values from the server
[RealtimeCallback(RealtimeModelEvent.OnDidRead)]
private void DidRead() {
if (eventDidFire != null && buttonPressed) // Check the buttonPressed property instead of the trigger property
eventDidFire(senderID, buttonNumber);
buttonPressed = false; // Reset the buttonPressed property to false
}
}
[RealtimeModel]
public partial class ButtonPressEventModel {
[RealtimeProperty(1, true)] private int _trigger;
[RealtimeProperty(2, true)] private int _senderID;
[RealtimeProperty(3, true)] private int _buttonNumber;
[RealtimeProperty(5, true)] private bool _buttonPressed; // New property

// Used to fire an event on all clients
public void FireEvent(int senderID, int buttonNumber) {
this.trigger++;
this.senderID = senderID;
this.buttonNumber = buttonNumber;
this.buttonPressed = true; // Set the buttonPressed property to true
}

// An event that consumers of this model can subscribe to in order to respond to the event
public delegate void EventHandler(int senderID, int buttonNumber);
public event EventHandler eventDidFire;
// A RealtimeCallback method that fires whenever we read any values from the server
[RealtimeCallback(RealtimeModelEvent.OnDidRead)]
private void DidRead() {
if (eventDidFire != null && buttonPressed) // Check the buttonPressed property instead of the trigger property
eventDidFire(senderID, buttonNumber);
buttonPressed = false; // Reset the buttonPressed property to false
}
}
This way, the buttonPressed property will ensure that the event is fired on all clients when the button is pressed, and the trigger property can still be used to keep track of how many times the button has been pressed. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai9mo ago
<@1081953198014734357> kapa.ai is still learning and improving, please let us know how it did by reacting below
Solution
Mechabit
Mechabit9mo ago
you should give up on the rpc recipe, instead sync the the menu state as an int and use the model changed event to update the menu on clients
Mechabit
Mechabit9mo ago
or if the button doesn't change any states you can just increment an int and you just listen to the update event