Photon Changing Entity or Model Every new Player joins the game:

Hi, I’m using Photon for my multiplayer game, where every time a player joins the room, their model or entity changes. However, it seems that only the ‘other player’ has their model or entity changed. Player 1’s model or entity does not change, even though its index updates.

PhotonLoadBalancingPlayCanvas.prototype.initialize = function () {
    // Initialize Photon client
    this.loadBalancingClient = new Photon.LoadBalancing.LoadBalancingClient(this.wss ? 1 : 0, this.appId, this.appVersion);
    this.loadBalancingClient.app = this.app;

    // Connect to Photon
    if (!this.loadBalancingClient.isInLobby()) {
        this.loadBalancingClient.connectToRegionMaster(this.region);
    }

    // Event listeners for Photon
    this.loadBalancingClient.onRoomList = this.onRoomList.bind(this);
    this.loadBalancingClient.onJoinRoom = this.onJoinRoom.bind(this);
    this.loadBalancingClient.onActorJoin = this.onActorJoin.bind(this);
    this.loadBalancingClient.onActorLeave = this.onActorLeave.bind(this);
    this.loadBalancingClient.onEvent = this.onEvent.bind(this);

    // PlayCanvas events
    this.app.on("createOtherPlayerEntity", this.createOtherPlayerEntity, this);
    this.app.on("UpdatePosition", this.sendPlayerPosition, this);

    // Track Shift key state
    this.isShiftPressed = false;
    this.app.keyboard.on(pc.EVENT_KEYDOWN, this.onKeyDown, this);
    this.app.keyboard.on(pc.EVENT_KEYUP, this.onKeyUp, this);

    // Avatar list
    this.playerEntitiesToClone = [
        { player1: this.app.root.findByName("Avatar1"), player2: this.app.root.findByName("Avatar1Clone") },
        { player1: this.app.root.findByName("Avatar2"), player2: this.app.root.findByName("Avatar2Clone") },
        { player1: this.app.root.findByName("Avatar3"), player2: this.app.root.findByName("Avatar3Clone") },
        { player1: this.app.root.findByName("Avatar4"), player2: this.app.root.findByName("Avatar4Clone") },
        { player1: this.app.root.findByName("Avatar5"), player2: this.app.root.findByName("Avatar5Clone") },
        { player1: this.app.root.findByName("Avatar6"), player2: this.app.root.findByName("Avatar6Clone") },
        { player1: this.app.root.findByName("Avatar7"), player2: this.app.root.findByName("Avatar7Clone") },
        { player1: this.app.root.findByName("Avatar8"), player2: this.app.root.findByName("Avatar8Clone") }
    ];

    if (this.playerEntitiesToClone.some(entry => !entry.player1 || !entry.player2)) {
        console.error("Error: One or more avatars are missing from the scene.");
        return;
    }

    // Set initial avatar index for the first player
    this.selectedAvatarIndex = 1;  // Start with Avatar1
    this.selectedAvatar = this.playerEntitiesToClone[this.selectedAvatarIndex];
};

PhotonLoadBalancingPlayCanvas.prototype.onRoomList = function () {
    this.loadBalancingClient.joinRandomOrCreateRoom();
};

PhotonLoadBalancingPlayCanvas.prototype.onJoinRoom = function (createRoom) {
    this.loadBalancingClient.myRoomActorsArray().forEach((actor) => {
        if (actor.isLocal) return;
        this.app.fire("createOtherPlayerEntity", actor);
    });

    // Increment the avatar index for Player 1 when they join (check if they created the room)
    if (createRoom) {
        // If Player 1 is creating the room, increment the avatar index
        this.selectedAvatarIndex = (this.selectedAvatarIndex + 1) % this.playerEntitiesToClone.length;
        this.selectedAvatar = this.playerEntitiesToClone[this.selectedAvatarIndex];
    }

    // Clone selected avatar for Player1
    if (!this.selectedAvatar.player1) {
        console.error("Error: Selected player entity not found.");
        return;
    }

    const entity1 = this.selectedAvatar.player1.clone();
    entity1.enabled = true;
    this.app.root.addChild(entity1);

    const { x, y, z } = entity1.getPosition();
    const { x: rx, y: ry, z: rz } = entity1.getEulerAngles();
    this.sendPlayerPosition({ x, y, z, rx, ry, rz });
};

PhotonLoadBalancingPlayCanvas.prototype.onActorJoin = function (actor) {
    // Increment the avatar index for every player who joins
    if (!actor.isLocal) {
        this.selectedAvatarIndex = (this.selectedAvatarIndex + 1) % this.playerEntitiesToClone.length;
        this.selectedAvatar = this.playerEntitiesToClone[this.selectedAvatarIndex];
    }

    if (actor.isLocal) return;
    this.app.fire("createOtherPlayerEntity", actor);

    const { x, y, z } = this.selectedAvatar.player1.getPosition();
    const { x: rx, y: ry, z: rz } = this.selectedAvatar.player1.getEulerAngles();
    this.app.fire("UpdatePosition", { x, y, z, rx, ry, rz });
};


PhotonLoadBalancingPlayCanvas.prototype.onActorLeave = function (actor) {
    const { actorNr } = actor;
    const otherPlayer = this.app.root.findByName(actorNr);
    if (!otherPlayer) return;
    otherPlayer.destroy();
};

PhotonLoadBalancingPlayCanvas.prototype.createOtherPlayerEntity = function (actor) {
    const { actorNr } = actor;

    if (!this.selectedAvatar.player2) {
        console.error("Error: Cloned player entity not found.");
        return;
    }

    const entity2 = this.selectedAvatar.player2.clone();
    entity2.enabled = true;
    entity2.name = actorNr;

    this.app.root.children[0].addChild(entity2);
};

PhotonLoadBalancingPlayCanvas.prototype.sendPlayerPosition = function (transform) {
    // Raise event for position update
    this.loadBalancingClient.raiseEvent(1, transform);

    // Check if the player is walking (based on position change)
    const isWalk = transform.x !== this.lastPosition?.x || transform.z !== this.lastPosition?.z;
    this.lastPosition = { x: transform.x, z: transform.z };

    // Check if the player is running (based on Shift key press)
    const isRun = this.isShiftPressed;

    // Raise event for walking/running state
    this.loadBalancingClient.raiseEvent(2, { actorNr: this.loadBalancingClient.myActor().actorNr, isWalk, isRun });

    // If not walking and not running, explicitly set isWalk and isRun to false
    if (!isWalk && !this.isShiftPressed) {
        this.loadBalancingClient.raiseEvent(2, { actorNr: this.loadBalancingClient.myActor().actorNr, isWalk: false, isRun: false });
    }
};

PhotonLoadBalancingPlayCanvas.prototype.onEvent = function (code, content, actorNr) {
    switch (code) {
        case 1: {
            const otherPlayer = this.app.root.findByName(actorNr);
            if (otherPlayer) {
                const { x, y, z, rx, ry, rz } = content;
                otherPlayer.setLocalPosition(x, y, z);
                otherPlayer.setEulerAngles(rx, ry, rz);
            }
            break;
        }
        case 2: {
            this.app.fire("playWalk", content.actorNr, content.isWalk);
            this.app.fire("playRun", content.actorNr, content.isRun);
            break;
        }
    }
};

PhotonLoadBalancingPlayCanvas.prototype.onKeyDown = function (event) {
    if (event.key === pc.KEY_SHIFT) {
        this.isShiftPressed = true;
    }
};

PhotonLoadBalancingPlayCanvas.prototype.onKeyUp = function (event) {
    if (event.key === pc.KEY_SHIFT) {
        this.isShiftPressed = false;
    }
};

player 1 does not change it model/entity

Hi @Cyrlon_Ian_Cayabyab!

Can I see the result with three players with a custom avatar?

I guess the avatar with the white shirt is the default one?

Hi @Albertos here are the photos.

Do you think I did something wrong in incrementing my array?



When you start the script/game, the index is 0. When a player joins or we join a room that we created, we increment to the next avatar. This is a problem because:

The avatars won’t be consistent if the player joins with other people in the room.
Everyone else’s index will be different because you start the index at 1 but never account for the people already in the room.

Player1 plays, sets their index to 1, and creates a room, increasing it to 2.
Player2 plays, sets their index to 1, and joins the room, still at 1.

You can see that the players’ indexes are not synced, causing the local player avatar to always be the same.

In my game, I use a host player (probably the room creator) to handle server-like code like a global avatar index.

(Btw start of array is index 0 unless you want to start at 1)
Here’s how I’d do it:
Host creates room, index is 1, creates its own avatar
Player2 joins room, host increases its index by 1, now 2, host creates their avatar in host scene and raises an event with the target player (Player2) and avatar index (2). Other people + Player2 creates avatar.

This happens so that other people and Player2 and host can create Player2’s avatar, everyone should now have consistent avatars.

The host should also set a custom property to the actor that joined so that when someone joins, they can loop through the players and see the avatar index to create the correct avatar.

If the avatar does not exist yet because the event is still going through but you have received a position update, just make sure that an entity exists with that name, if not, return.

Hope this is what you were looking for, feel free to ask any questions!

1 Like

Thank you so much. Will try this, and can I have some reference for making a Host player in play canvas for learning purposes. Cause I did not try it yet. all i have now are all client and the host is the server(Photon)

I’m making a game with a host player but its kind of a mess right now, you can check main-menu-photon.js and game-photon.js here.
My game uses the host player to do server actions because we haven’t set up our own server. (Setting up your own server is also another option.) It handles the entity ai and things like that which does not require every client to run.
When a client creates a room, that client sets a room custom property as their actor number. I also tell the host that they are the host by setting a local variable for easy access.

// Upon local client joining room
if(createdByMe){
    room.setCustomProperty("hostActorNr", myActor.actorNr)
    myActor.isHost = true
}

If a client needs to know that they are the host, they use myActor.isHost. If a client needs to know who the host is, they can use the room custom property.

For your situation, the host has a local variable for the avatar index. When a client joins. The host increases the index and sends it through an event. Each client should store their index through their actor custom properties so that others can access it to create the same consistent avatars.

// loadBalancingClient.onJoinRoom
if(createdByMe){
    // ...
    this.avatarIndex = 0
    // Create Avatar here
}else{
    // Get everyone's avatar index by looping through actors and looking at their custom property here
}

// loadBalancingClient.onEvent
case avatarIndexEvent:
    if(content.toActorNr === myActor.actorNr){
        // Create our local avatar here
    }else{
        // Create the other player avatar here
    }

// loadBalancingClient.onActorJoin
if(myActor.isHost){
    // Increment avatar index here
    joinedActor.setCustomProperty("avatarIndex", content.avatarIndex)
 
   loadBalancingClient.raiseEvent(avatarIndexEvent, { toActorNr: joinedActor.actorNr, avatarIndex: this.avatarIndex })
    // Create the avatar here because raiseEvent does not go to the sender
}

If you need me to create a full on example, let me know and I will do so soon.

1 Like

Wow, thank you! I’m learning so much from you. If you insist giving me the example, I would also like to have the full example for learning purposes. I will also try this. Thank you again; you really help me.

Here’s the example: PlayCanvas | HTML5 Game Engine
If you have any questions, let me know! :grin:

2 Likes

Gee, thanks! I really appreciate it. Thank you also for the comments on the project; they really helped me understand. It’s like I’m talking to a friend. Thanks again!! Will also let you know if I have some question/s.

1 Like

Happy to help!