ncsoft / Unreal.js

Unreal.js: Javascript runtime built for UnrealEngine
Other
3.67k stars 351 forks source link

Replication (or TextRender component) in JS not working the same as in BP #63

Closed Youdaman closed 8 years ago

Youdaman commented 8 years ago

I followed this official tutorial about replication -- https://www.youtube.com/watch?v=htdI2dh-aek -- and implemented things up to the 17 minute mark, with success:

bp

When I tried to implement the same logic in JS however things do not work the same:

js

Here is my JS code which I believe has the same logic as the tutorial uses for BP:

(function (global) {
  "use strict"

  class MyCharacter extends Blueprint.Load('/Game/ThirdPersonBP/Blueprints/ThirdPersonCharacter').GeneratedClass {
      ctor() {
        this.maxHealth = 100
        this.maxBombCount = 3
      }
      ReceiveBeginPlay(DeltaTime) {
        //if (this.Role == 'ROLE_Authority') {
        if (this.HasAuthority()) {
          this.InitAttributes()
        }
      }
      InitAttributes() {
        if (this.HasAuthority()) {
          this.InitHealth();
          this.InitBombs();
        }
      }
      InitHealth() {
        this.health = this.maxHealth
        console.log('health updated by', this.Role)
      }
      InitBombs() {
        this.bombCount = this.maxBombCount
        console.log('bombCount updated by', this.Role)
      }
      UpdateCharacterDisplayText() {
        let text = 'Health: ' + this.health + ' Bombs: ' + this.bombCount
        this.TextRender.SetText(text)
        console.log('text updated by', this.Role, '=', text)
      }
      OnRepHealth()/*Any*/ {
        this.UpdateCharacterDisplayText()
      }
      OnRepBombCount()/*Any*/ {
        this.UpdateCharacterDisplayText()
      }
      properties() {
        this.health/*ReplicatedUsing:OnRepHealth+float*/; // not usually RepNotify but is for the TextRender
        this.maxHealth/*float*/;
        this.bombCount/*ReplicatedUsing:OnRepBombCount+int*/;
        this.maxBombCount/*int*/;
      }
  }
  let MyCharacter_C = require('uclass')()(global,MyCharacter)
  Root.GetOuter().DefaultPawnClass = MyCharacter_C

  function main() {
    return function() {}
  }

  try {
    module.exports = () => {
      let cleanup = null
      cleanup = main();
      return () => cleanup()
    }
  }
  catch (e) {
    require('bootstrap')('game')
  }
})(this)

You can see the console output in the screenshot above is correctly updating the health and bombCount only on the server, and changing the text render component on the client, and this text value is correctly set to "Health: 100 Bombs: 3" however the text render component on the server is not changing.

My only guess is `this.TextRender.SetText(text)' is not the right way to grab the text render component and set it?

Youdaman commented 8 years ago

I updated UpdateCharacterDisplayText() to also output the name of the actor:

console.log('text updated by', this.Role, this.GetName(), '=', text)

And in the console I see:

< text updated by ROLE_AutonomousProxy MyCharacter_C1_1 = Health: 100 Bombs: 3
< text updated by ROLE_AutonomousProxy MyCharacter_C1_1 = Health: 100 Bombs: 3

Which tells me that only the one character is updating the text (MyCharacter_C1_1) -- the reason the line appears twice is the function gets called by both OnHealthRep and OnBombCountRep.

It should also be getting called twice by MyCharacter_C1_0 which is the other character but it's not.

Youdaman commented 8 years ago

This post in the forums gives me a clue!

https://forums.unrealengine.com/showthread.php?3709-RepNotify-from-c-confusion&p=261293&viewfull=1#post261293

From the hint given there, I think I need to do something like this inside my InitHealth() function (and do the same for InitBombs()):

        if (this.GetNetMode() != 'NM_DedicatedServer'){
          this.OnRepHealth()
        }

However Unreal.js says TypeError: this.GetNetMode is not a function.

I've at least narrowed down the problem to needing to explicitly call the RepNotify function when you're on listen server -- I tried running 2 clients with a dedicated server and both updated fine.

Youdaman commented 8 years ago

Also tried GEngine.GetNetMode(GWorld) as I've seen the equivalent in C++ on some forum posts: GEngine->GetNetMode(GWorld) -- however it seems GetNetMode() isn't available via GEngine in Unreal.js?

nakosung commented 8 years ago

You can query whether you are in a server by calling GWorld.IsServer(). (please refer to https://github.com/ncsoft/Unreal.js/blob/master/Examples/Content/Scripts/helloReplication.js#L40)

Server never get notified by 'RepNotify' as you mentioned, so your server logic should call proper 'RepNotify' manually.

incHealth() {
  this.health = this.health + 1;
  if (GWorld.IsServer()) {
    this.OnHealth_RepNotify()
  }
}
Youdaman commented 8 years ago

Thanks nakosung -- I've changed the code as follows:

(function (global) {
  "use strict"

  class MyCharacter extends Blueprint.Load('/Game/ThirdPersonBP/Blueprints/ThirdPersonCharacter').GeneratedClass {
      ctor() {
        this.maxHealth = 100
        this.maxBombCount = 3
      }
      ReceiveBeginPlay(DeltaTime) {
        //if (this.Role == 'ROLE_Authority') {
        if (this.HasAuthority()) {
          this.InitAttributes()
        }
        global.character = this
      }
      InitAttributes() {
        if (this.HasAuthority()) {
          this.InitHealth();
          this.InitBombs();
        }
      }
      InitHealth() {
        this.health = this.maxHealth
        console.log('health', this.health, 'updated by', this.Role, this.GetName())
        if (GWorld.IsServer()){
          console.log('manually calling OnRepHealth()')
          this.OnRepHealth()
        }
      }
      InitBombs() {
        this.bombCount = this.maxBombCount
        console.log('bombCount', this.bombCount, 'updated by', this.Role, this.GetName())
        if (GWorld.IsServer()){
          console.log('manually calling OnRepBombCount()')
          this.OnRepBombCount()
        }
      }
      UpdateCharacterDisplayText() {
        let text = 'Health: ' + this.health + ' Bombs: ' + this.bombCount
        this.TextRender.SetText(text)
        console.log('text updated by', this.Role, this.GetName(), '=', text)
      }
      OnRepHealth()/*Any*/ {
        console.log('in OnRepHealth GWorld.IsServer() =', GWorld.IsServer(), this.Role, this.GetName())
        this.UpdateCharacterDisplayText()
      }
      OnRepBombCount()/*Any*/ {
        console.log('in OnRepBombCount GWorld.IsServer() =', GWorld.IsServer(), this.Role, this.GetName())
        this.UpdateCharacterDisplayText()
      }
      properties() {
        this.health/*ReplicatedUsing:OnRepHealth+float*/; // not usually RepNotify but is for the TextRender
        this.maxHealth/*float*/;
        this.bombCount/*ReplicatedUsing:OnRepBombCount+int*/;
        this.maxBombCount/*int*/;
      }
  }
  let MyCharacter_C = require('uclass')()(global,MyCharacter)
  Root.GetOuter().DefaultPawnClass = MyCharacter_C

  function main() {
    return function() {}
  }

  try {
    module.exports = () => {
      let cleanup = null
      cleanup = main();
      return () => cleanup()
    }
  }
  catch (e) {
    require('bootstrap')('game')
  }
})(this)

However the text is still not updating which doesn't make sense as the RepNotify function is successfully being called manually:

< health 100 updated by ROLE_Authority MyCharacter_C1_0
< manually calling OnRepHealth()
< in OnRepHealth GWorld.IsServer() = true ROLE_Authority MyCharacter_C1_0
< text updated by ROLE_Authority MyCharacter_C1_0 = Health: 100 Bombs: 0
< bombCount 3 updated by ROLE_Authority MyCharacter_C1_0
< manually calling OnRepBombCount()
< in OnRepBombCount GWorld.IsServer() = true ROLE_Authority MyCharacter_C1_0
< text updated by ROLE_Authority MyCharacter_C1_0 = Health: 100 Bombs: 3
LogNet: Join succeeded: 279
< in OnRepBombCount GWorld.IsServer() = true ROLE_AutonomousProxy MyCharacter_C1_1
< text updated by ROLE_AutonomousProxy MyCharacter_C1_1 = Health: 100 Bombs: 3
< in OnRepHealth GWorld.IsServer() = true ROLE_AutonomousProxy MyCharacter_C1_1
< text updated by ROLE_AutonomousProxy MyCharacter_C1_1 = Health: 100 Bombs: 3

screenshot 2016-04-14 10 39 11

One thing I noticed in the output above is that the first time UpdateCharacterDisplayText() is called this.bombCount is 0 because its not been changed yet -- in the ctor() I set health first, then bombCount, so this makes sense, and I assume MyCharacter_C1_0 is on the Listen server.

What doesn't make sense (to me) is that when MyCharacter_C1_1 joins and sets its properties, this.bombCount is already 3 (from when MyCharacter_C1_0 set it I assume) but shouldn't it be 0 the same as it was for MyCharacter_C1_0 initially?

So it seems GWorld.IsServer() does the right thing, but for whatever reason the TextRenderComponent on the Listen server is not updating -- it does update in BP though, and the thing with bombCount being already set to 3 for the second character might be a clue? It's like the second instance of the Character can see the properties of the second instance -- does this mean they're being set on the class itself in Unreal.js rather than the insance?

I could be completely on the wrong track but just wondering a) why the text doesn't update for the listen server and b) why the second instance of the Character already has its bombCount set.

Youdaman commented 8 years ago

Just noticed that these two lines for the first Character instance don't get logged by the second:

< health 100 updated by ROLE_Authority MyCharacter_C1_0
< bombCount 3 updated by ROLE_Authority MyCharacter_C1_0

Will try replacing this.HasAuthority() with this.Role == 'ROLE_Authority' brb.

Youdaman commented 8 years ago

No luck. Even tried if (this.Role == 'ROLE_Authority' || this.Role == 'ROLE_AutonomousProxy') { but the Text Render Component still didn't update even though the log is saying it did.

Youdaman commented 8 years ago

Here is the log output when using if (this.Role == 'ROLE_Authority' || this.Role == 'ROLE_AutonomousProxy') instead of if (this.HasAuthority()) in both ReceiveBeginPlay() and InitAttributes():

LogNet: Join request: /Game/ThirdPersonBP/Maps/ThirdPersonExampleMap?SplitscreenCount=1
< BeginPlay() event for ROLE_Authority MyCharacter_C1_0
< InitAttributes() for ROLE_Authority MyCharacter_C1_0
< health 100 updated by ROLE_Authority MyCharacter_C1_0
< manually calling OnRepHealth()
< in OnRepHealth GWorld.IsServer() = true ROLE_Authority MyCharacter_C1_0
< text updated by ROLE_Authority MyCharacter_C1_0 = Health: 100 Bombs: 0
< bombCount 3 updated by ROLE_Authority MyCharacter_C1_0
< manually calling OnRepBombCount()
< in OnRepBombCount GWorld.IsServer() = true ROLE_Authority MyCharacter_C1_0
< text updated by ROLE_Authority MyCharacter_C1_0 = Health: 100 Bombs: 3
LogNet: Join succeeded: 303
< in OnRepBombCount GWorld.IsServer() = true ROLE_AutonomousProxy MyCharacter_C1_1
< text updated by ROLE_AutonomousProxy MyCharacter_C1_1 = Health: 100 Bombs: 3
< in OnRepHealth GWorld.IsServer() = true ROLE_AutonomousProxy MyCharacter_C1_1
< text updated by ROLE_AutonomousProxy MyCharacter_C1_1 = Health: 100 Bombs: 3
< BeginPlay() event for ROLE_AutonomousProxy MyCharacter_C1_1
< InitAttributes() for ROLE_AutonomousProxy MyCharacter_C1_1
< health 100 updated by ROLE_AutonomousProxy MyCharacter_C1_1
< manually calling OnRepHealth()
< in OnRepHealth GWorld.IsServer() = true ROLE_AutonomousProxy MyCharacter_C1_1
< text updated by ROLE_AutonomousProxy MyCharacter_C1_1 = Health: 100 Bombs: 3
< bombCount 3 updated by ROLE_AutonomousProxy MyCharacter_C1_1
< manually calling OnRepBombCount()
< in OnRepBombCount GWorld.IsServer() = true ROLE_AutonomousProxy MyCharacter_C1_1
< text updated by ROLE_AutonomousProxy MyCharacter_C1_1 = Health: 100 Bombs: 3

So it looks like the Character on the Listen server which has ROLE_AutonomousProxy is now correctly calling the Init functions but for whatever reason the TextRenderComponent does not change and still shows the default "Text" as per previous screenshots.

Youdaman commented 8 years ago

One problem I can see with the above console output is:

LogNet: Join succeeded: 303
< in OnRepBombCount GWorld.IsServer() = true ROLE_AutonomousProxy MyCharacter_C1_1
< text updated by ROLE_AutonomousProxy MyCharacter_C1_1 = Health: 100 Bombs: 3
< in OnRepHealth GWorld.IsServer() = true ROLE_AutonomousProxy MyCharacter_C1_1
< text updated by ROLE_AutonomousProxy MyCharacter_C1_1 = Health: 100 Bombs: 3
< BeginPlay() event for ROLE_AutonomousProxy MyCharacter_C1_1

The RepNotify functions are being called before the BeginPlay event, I'm guessing when the properties are set in the ctor() -- and this is probably why the second call does not update the Text because the values are unchanged!

Will look at the docs to see if I can set defaults in properties() instead brb :)

Youdaman commented 8 years ago

Nope. I tried setting the defaults in ReceiveBeginPlay() instead of ctor as follows:

//       ctor() {
//         this.maxHealth = 100
//         this.maxBombCount = 3
//       }
      ReceiveBeginPlay(DeltaTime) {
        console.log('BeginPlay() event for', this.Role, this.GetName())
        this.maxHealth = 100
        this.maxBombCount = 3
        //if (this.HasAuthority()) {
        if (this.Role == 'ROLE_Authority' || this.Role == 'ROLE_AutonomousProxy') {
          this.InitAttributes()
        }
      }

And yet the log still shows the RepNotify functions being called before the BeginPlay event for the joining client:

LogNet: Join request: /Game/ThirdPersonBP/Maps/ThirdPersonExampleMap?SplitscreenCount=1
< BeginPlay() event for ROLE_Authority MyCharacter_C1_0
< InitAttributes() for ROLE_Authority MyCharacter_C1_0
< health 100 updated by ROLE_Authority MyCharacter_C1_0
< manually calling OnRepHealth()
< in OnRepHealth GWorld.IsServer() = true ROLE_Authority MyCharacter_C1_0
< text updated by ROLE_Authority MyCharacter_C1_0 = Health: 100 Bombs: 0
< bombCount 3 updated by ROLE_Authority MyCharacter_C1_0
< manually calling OnRepBombCount()
< in OnRepBombCount GWorld.IsServer() = true ROLE_Authority MyCharacter_C1_0
< text updated by ROLE_Authority MyCharacter_C1_0 = Health: 100 Bombs: 3
LogNet: Join succeeded: 263
< in OnRepBombCount GWorld.IsServer() = true ROLE_AutonomousProxy MyCharacter_C1_1
< text updated by ROLE_AutonomousProxy MyCharacter_C1_1 = Health: 100 Bombs: 3
< in OnRepHealth GWorld.IsServer() = true ROLE_AutonomousProxy MyCharacter_C1_1
< text updated by ROLE_AutonomousProxy MyCharacter_C1_1 = Health: 100 Bombs: 3
< BeginPlay() event for ROLE_AutonomousProxy MyCharacter_C1_1
< InitAttributes() for ROLE_AutonomousProxy MyCharacter_C1_1
< health 100 updated by ROLE_AutonomousProxy MyCharacter_C1_1
< manually calling OnRepHealth()
< in OnRepHealth GWorld.IsServer() = true ROLE_AutonomousProxy MyCharacter_C1_1
< text updated by ROLE_AutonomousProxy MyCharacter_C1_1 = Health: 100 Bombs: 3
< bombCount 3 updated by ROLE_AutonomousProxy MyCharacter_C1_1
< manually calling OnRepBombCount()
< in OnRepBombCount GWorld.IsServer() = true ROLE_AutonomousProxy MyCharacter_C1_1
< text updated by ROLE_AutonomousProxy MyCharacter_C1_1 = Health: 100 Bombs: 3

These lines in particular:

LogNet: Join succeeded: 263
< in OnRepBombCount GWorld.IsServer() = true ROLE_AutonomousProxy MyCharacter_C1_1
< text updated by ROLE_AutonomousProxy MyCharacter_C1_1 = Health: 100 Bombs: 3
< in OnRepHealth GWorld.IsServer() = true ROLE_AutonomousProxy MyCharacter_C1_1
< text updated by ROLE_AutonomousProxy MyCharacter_C1_1 = Health: 100 Bombs: 3
< BeginPlay() event for ROLE_AutonomousProxy MyCharacter_C1_1

So my guess is that subsequent instances of JS classes are getting their properties set using values from previous instances? Is this a bug?

Possibly caused by the way JS does prototype inheritance?

Youdaman commented 8 years ago

Actually no, I think I get why the RepNotify triggers before BeginPlay for the second Character -- it's because it's a new client joining and it needs to see the changes of the first Character and so the game replicates/triggers those updates yeah?

Youdaman commented 8 years ago

I've had some success but I don't know why -- here is my code, reverted back to how it was with the this.HasAuthority() and GWorld.IsServer() logic:

(function (global) {
  "use strict"

  class MyCharacter extends Blueprint.Load('/Game/ThirdPersonBP/Blueprints/ThirdPersonCharacter').GeneratedClass {
      ctor() {
        console.log('start ctor() for', this.Role, this.GetName())
        this.maxHealth = 100
        this.maxBombCount = 3
        console.log('end ctor() for', this.Role, this.GetName())
      }
      ReceiveBeginPlay(DeltaTime) {
        console.log('BeginPlay() event for', this.Role, this.GetName())
        //if (this.Role == 'ROLE_Authority') {
        //if (this.Role == 'ROLE_Authority' || this.Role == 'ROLE_AutonomousProxy') {
        if (this.HasAuthority()) {
          this.InitAttributes()
        }
      }
      InitAttributes() {
        console.log('InitAttributes() for', this.Role, this.GetName())
        //if (this.Role == 'ROLE_Authority') {
        //if (this.Role == 'ROLE_Authority' || this.Role == 'ROLE_AutonomousProxy') {
        if (this.HasAuthority()) {
          this.InitHealth();
          this.InitBombs();
        }
      }
      InitHealth() {
        this.health = this.maxHealth
        console.log('health', this.health, 'updated by', this.Role, this.GetName())
        if (GWorld.IsServer()){
          console.log('manually calling OnRepHealth()')
          this.OnRepHealth()
        }
      }
      InitBombs() {
        this.bombCount = this.maxBombCount
        console.log('bombCount', this.bombCount, 'updated by', this.Role, this.GetName())
        if (GWorld.IsServer()){
          console.log('manually calling OnRepBombCount()')
          this.OnRepBombCount()
        }
      }
      UpdateCharacterDisplayText() {
        let text = 'Health: ' + this.health + ' Bombs: ' + this.bombCount
        this.TextRender.SetText(text)
        console.log('text updated by', this.Role, this.GetName(), '=', text)
      }
      OnRepHealth()/*Any*/ {
        console.log('in OnRepHealth GWorld.IsServer() =', GWorld.IsServer(), this.Role, this.GetName())
        this.UpdateCharacterDisplayText()
      }
      OnRepBombCount()/*Any*/ {
        console.log('in OnRepBombCount GWorld.IsServer() =', GWorld.IsServer(), this.Role, this.GetName())
        this.UpdateCharacterDisplayText()
      }
      properties() {
        this.health/*ReplicatedUsing:OnRepHealth+float*/; // not usually RepNotify but is for the TextRender
        this.maxHealth/*float*/;
        this.bombCount/*ReplicatedUsing:OnRepBombCount+int*/;
        this.maxBombCount/*int*/;
      }
  }
  let MyCharacter_C = require('uclass')()(global,MyCharacter)
  Root.GetOuter().DefaultPawnClass = MyCharacter_C

  function main() {
    return function() {}
  }

  try {
    module.exports = () => {
      let cleanup = null
      cleanup = main();
      return () => cleanup()
    }
  }
  catch (e) {
    require('bootstrap')('game')
  }
})(this)

And here is the console output:

LogNet: Join request: /Game/ThirdPersonBP/Maps/ThirdPersonExampleMap?SplitscreenCount=1
< start ctor() for ROLE_Authority MyCharacter_C1_1
< end ctor() for ROLE_Authority MyCharacter_C1_1
< BeginPlay() event for ROLE_Authority MyCharacter_C1_1
< InitAttributes() for ROLE_Authority MyCharacter_C1_1
< health 100 updated by ROLE_Authority MyCharacter_C1_1
< manually calling OnRepHealth()
< in OnRepHealth GWorld.IsServer() = true ROLE_Authority MyCharacter_C1_1
< text updated by ROLE_Authority MyCharacter_C1_1 = Health: 100 Bombs: 0
< bombCount 3 updated by ROLE_Authority MyCharacter_C1_1
< manually calling OnRepBombCount()
< in OnRepBombCount GWorld.IsServer() = true ROLE_Authority MyCharacter_C1_1
< text updated by ROLE_Authority MyCharacter_C1_1 = Health: 100 Bombs: 3
LogNet: Join succeeded: 277
< start ctor() for ROLE_Authority MyCharacter_C1_2
< end ctor() for ROLE_Authority MyCharacter_C1_2
< in OnRepBombCount GWorld.IsServer() = true ROLE_AutonomousProxy MyCharacter_C1_2
< text updated by ROLE_AutonomousProxy MyCharacter_C1_2 = Health: 100 Bombs: 3
< in OnRepHealth GWorld.IsServer() = true ROLE_AutonomousProxy MyCharacter_C1_2
< text updated by ROLE_AutonomousProxy MyCharacter_C1_2 = Health: 100 Bombs: 3
< start ctor() for ROLE_Authority MyCharacter_C1_3
< end ctor() for ROLE_Authority MyCharacter_C1_3
< in OnRepBombCount GWorld.IsServer() = true ROLE_SimulatedProxy MyCharacter_C1_3
< text updated by ROLE_SimulatedProxy MyCharacter_C1_3 = Health: 100 Bombs: 3
< in OnRepHealth GWorld.IsServer() = true ROLE_SimulatedProxy MyCharacter_C1_3
< text updated by ROLE_SimulatedProxy MyCharacter_C1_3 = Health: 100 Bombs: 3
< BeginPlay() event for ROLE_AutonomousProxy MyCharacter_C1_2
< BeginPlay() event for ROLE_SimulatedProxy MyCharacter_C1_3

And here is a screenshot:

screenshot 2016-04-14 11 57 45

I achieved this by deleting the ThirdPersonCharacter that was in the level -- I think the problem was that it was being used as the Listen Server's character whereas the client that connected was a pure MyCharacter inherited from ThirdPersonCharacter.

So maybe to make that ThirdPersonCharacter that's in the level by default work properly I should have cast it to be a MyCharacter? Or just not have it at all like I discovered and instead spawn them all from JS to save the hassle :)

Anyway I think that was the problem -- inheritance/extending but not having a proper instance as the Listen Server's character.

nakosung commented 8 years ago

A really long posts! Congratulations for what you have made!

Currently Javascript generated class is not a public class which can be placed by level editor, and super class(ThirdPersonCharacter) cannot be casted into child class(MyCharacter).

Youdaman commented 8 years ago

Thank you :)