Forum Discussion

EchoExclusive's avatar
9 months ago
Solved

Player Specific Custom UI - bindings

Hi Creators,

I am trying to do player specific UIs with bindings. I'm following this but I think I'm not understanding it correctly. https://developers.meta.com/horizon-worlds/learn/documentation/desktop-editor/custom-ui/playerspecific-custom-ui

My goal is each user has health. If they collect a heart they will gain +1 health and display it on their custom UI. When I am testing with my teammate. We both start of as 0 (I know, 0 doesn't make sense for health, but for testing purposes let's start at 0 πŸ˜€) If bindings isn't the way to go, what could I do? If it should be bindings, does anyone know what I'm doing wrong? Example below.

 

When I collect 2 hearts, my UI will say 2, teammate will say 0. So far so good ❀️

Now my teammate collects 1 heart, my UI will say 2, teammate will say 3, NOT good 😭 teammate should say 1

If I collect 1 heart, my UI will say 4, teammate will say 3. At this point the count is off  for everyone. 😭

It seems like it's adding on based the last value.

 

Here is my code below. The first script is the trigger attached to a trigger for the heart

import { Component, CodeBlockEvents,Player, PropTypes} from 'horizon/core';
import { collectHealth } from 'HeadsUpDisplay';
class CollectibleItems extends Component<typeof CollectibleItems> {
  static propsDefinition = {
    numberToUpgrade: {type: PropTypes.Number, default:1.0},
  };

  start() {
      this.connectCodeBlockEvent(this.entity, CodeBlockEvents.OnPlayerEnterTrigger,(player:Player)=>{
        this.playerEnterTrigger(player);
      })
  }

  playerEnterTrigger(player:Player)
  {
      this.sendLocalBroadcastEvent(
      collectHealth,
      {health: this.props.numberToUpgrade, player: player}
    );
  }
}
Component.register(CollectibleItems);

Next is my custom UI that displays the health - which appears to work perfectly for one player. Multiplayer goes wonky. So I probably am misunderstanding how binding works

import { CodeBlockEvents, Color, Component, Player, PropTypes, TextureAsset, LocalEvent } from 'horizon/core';
import {
    UIComponent,
    View,
    Text,
    Binding,
    UINode,

} from "horizon/ui";

export const collectHealth = new LocalEvent<{health: number, player:Player}>('collectHealth');


class HeadsUpDisplay extends UIComponent<typeof HeadsUpDisplay> {
    static propsDefinition = {
        maxHealthNumber: {type: PropTypes.Number},
          
    };

    private healthCount = 0;
    initializeUI(): UINode {
        
          const healthTitle = View({
            children: [   
            Text({
                text: this.strPlayerHealthTotal,
                style: {
                    fontSize: 24,
                    fontFamily: 'Optimistic',
                    color: 'white',

                },
            }),],
            style: {
                flexDirection: 'row',
            },
          });


        return View({
            children: [healthTitle
               
            ],
            style: {


            },
        })
    }


    start() {
        this.connectLocalBroadcastEvent(collectHealth,(data:{health:number,player:Player})=> {
            this.handleHealthUpdate(data.health, data.player);
        })
    }

    strPlayerHealthTotal = new Binding<string>("0");


    private handleHealthUpdate(health: number, player: Player): void {


        this.healthCount++;
        console.log("total health" + this.healthCount, ", player:" + player.name.get());     
        this.strPlayerHealthTotal.set(this.healthCount.toString(),[player]);
    }


}
UIComponent.register(HeadsUpDisplay);

 

 

  • So combining the responses from above and this tutorial https://communityforums.atmeta.com/t5/Community-Resources/Mobile-Worlds-Crash-Course-with-Laex05-Now-Available/td-p/1303353. The solution I went with was I used a Map. The key is the player and the value is the stats.

     

    So player enters the world

    I have this:

            cuiStats_Data.playerStatsMap.set(player, playerStats);
    
            cuiStats_Data.playerStatsBinding.set(playerStats, [player]);
            this.connectNetworkEvent(player, collectHealth, (data: { health: number, player: Player }) => {
                this.handleHealthUpdate(data.health, data.player);
            })

    handleHealthUpdate looks like this

     

        private handleHealthUpdate(health: number, player: Player): void {
    
    
            var tempPlayerStats = cuiStats_Data.playerStatsMap.get(player);
            if (tempPlayerStats) 
            {
                tempPlayerStats.healthPercentage += health;
                cuiStats_Data.playerStatsBinding.set(tempPlayerStats, [player])
    
            }
        }

     

    This place where I would want to call this event, I would do

      this.sendNetworkEvent(player,
            collectHealth,
            {health: this.props.numberToUpgrade, player: player}
          );
        }

     

    Thank you to everyone who provided ideas and solutions!

5 Replies

  • Hello
    I am not experienced with this development technology but here are some ideas that might be helpful:

    1. Is the HeadsUpDisplay script running locally or on the server?

    2. Shouldn't the handleHealthUpdate method check that the player in the parameter is the local player of the device before changing the state?

    3. In general I don't think it's a good practice to store player's state (in this case the health value) inside the UI class.

    • EchoExclusive's avatar
      EchoExclusive
      Partner

      Thanks for the response. Those are great ideas I did not think about. 

      I guess my main question is why the binding is behaving the way I described

  • Since you want different stuff displayed on the HUD for each player it will be better to make it a local script. Since the hud script is a local script and the item is a server script you will have to use Network events and since you want to only send to the specific player you will not want to use a Broadcast event.

    Here is the CollectibleItems script with the updates.

    import { Component, CodeBlockEvents, Player, PropTypes, NetworkEvent } from 'horizon/core';
    
    class CollectibleItems extends Component<typeof CollectibleItems> {
         static propsDefinition = {
              numberToUpgrade: { type: PropTypes.Number, default: 1.0 },
         };
    
         start() {
              this.connectCodeBlockEvent(this.entity, CodeBlockEvents.OnPlayerEnterTrigger, (player: Player) => { this.playerEnterTrigger(player); })
         }
    
         playerEnterTrigger(player: Player) {
              // since the HUD is going to be a local script we cannot use a local event as the item and the hud are running on different clients.
              // and since we want only the player to receive the event we cannot use a broadcast event
              this.sendNetworkEvent(player, new NetworkEvent<{ health: number, player: Player }>('handleHealthUpdate'), { health: this.props.numberToUpgrade, player: player });
         }
    }
    Component.register(CollectibleItems);

     

    Here is the HeadsUpDisplay with the updates

    import { CodeBlockEvents, Player, PropTypes, NetworkEvent } from 'horizon/core';
    import { UIComponent, View, Text, Binding, UINode } from "horizon/ui";
    
    class HeadsUpDisplay extends UIComponent<typeof HeadsUpDisplay> {
         static propsDefinition = {
         maxHealthNumber: { type: PropTypes.Number },
         };
    
         private strPlayerHealthTotal = new Binding<string>("0");
    
         private healthCount = 0;
         private playerIndex: number = 0;
    
         private activePlayer: Player | undefined;
    
         initializeUI(): UINode {
              return View({
                   children: [
                        Text({
                             text: this.strPlayerHealthTotal,
                             style: { fontSize: 24, fontFamily: 'Optimistic', color: 'white', },
                        })
                   ],
                   style: {},
              })
         }
    
         preStart() {
              // Always connect in prestart unless it is for a player
              this.connectCodeBlockEvent(this.entity, CodeBlockEvents.OnPlayerEnterWorld, (player: Player) => { this.OnPlayerEnterWorld(player); })
              this.connectCodeBlockEvent(this.entity, CodeBlockEvents.OnPlayerExitWorld, (player: Player) => { this.OnPlayerExitWorld(player); })
         }
    
         start() {
              // The entities name is PlayerHud00
              // the following code gets the entities name and removes all but the last 2 characters
              // then turns it into a number type so we can set it to the playerIndex variable
              // I do it this way becasue it is easier to change an entities name than set a props def.
              const name = this.entity.name.get();
              const index = name.slice(name.length - 2);
              this.playerIndex = parseInt(index);
    
              this.activePlayer = this.entity.owner.get();
    
              // if the active player is not the server player we will connect to the handleHealthUpdate event.
              if (this.activePlayer !== this.world.getServerPlayer()) {
              this.connectNetworkEvent(this.activePlayer, new NetworkEvent<{ health: number, player: Player }>('handleHealthUpdate'), ({ health, player }) => this.handleHealthUpdate(health, player));
              }
              // if the active player is the server player we will disconnect from the handleHealthUpdate event.
              else {
              this.connectNetworkEvent(this.entity, new NetworkEvent<{ health: number, player: Player }>('handleHealthUpdate'), ({ health, player }) => this.handleHealthUpdate(health, player)).disconnect()
              }
         }
    
         OnPlayerEnterWorld(player: Player) {
              if (player.index.get() !== this.playerIndex) return;
    
              this.entity.owner.set(player);
         }
    
         OnPlayerExitWorld(player: Player) {
              if (player.index.get() !== this.playerIndex) return;
    
              this.entity.owner.set(this.world.getServerPlayer());
         }
    
         private handleHealthUpdate(health: number, player: Player): void {
              if (player !== this.activePlayer) return;
    
              // use += health to be able to subtract health with the same event.
              this.healthCount += health;
              this.strPlayerHealthTotal.set(this.healthCount.toString());
         }
    }
    UIComponent.register(HeadsUpDisplay);
  • Thanks for the response. I appreciate both of your responses!

    I'm combining both answers and trying to make my own solution that makes sense in my head.

  • So combining the responses from above and this tutorial https://communityforums.atmeta.com/t5/Community-Resources/Mobile-Worlds-Crash-Course-with-Laex05-Now-Available/td-p/1303353. The solution I went with was I used a Map. The key is the player and the value is the stats.

     

    So player enters the world

    I have this:

            cuiStats_Data.playerStatsMap.set(player, playerStats);
    
            cuiStats_Data.playerStatsBinding.set(playerStats, [player]);
            this.connectNetworkEvent(player, collectHealth, (data: { health: number, player: Player }) => {
                this.handleHealthUpdate(data.health, data.player);
            })

    handleHealthUpdate looks like this

     

        private handleHealthUpdate(health: number, player: Player): void {
    
    
            var tempPlayerStats = cuiStats_Data.playerStatsMap.get(player);
            if (tempPlayerStats) 
            {
                tempPlayerStats.healthPercentage += health;
                cuiStats_Data.playerStatsBinding.set(tempPlayerStats, [player])
    
            }
        }

     

    This place where I would want to call this event, I would do

      this.sendNetworkEvent(player,
            collectHealth,
            {health: this.props.numberToUpgrade, player: player}
          );
        }

     

    Thank you to everyone who provided ideas and solutions!