Zum Inhalt wechseln

Change

Referenzierung nicht möglich/Wann ist Szene fertig geladen?


11 replies to this topic

#1
Lynxxx

    Newbie

  • Members
  • PIP
  • 7 Beiträge:
Hallo

Ich arbeite gerade an einem Projekt für meine Bachelor-Arbeit. Dabei bin ich jedoch auf ein Problem gestossen, welches ich auch nach mehreren Stunden Recherche und ausprobieren nicht lösen konnte. Entsprechend wende ich mich an euch, in der Hoffnung, dass ich einfach einen dummen Fehler gemacht habe, bzw. einer von euch mir weiterhelfen kann. Danke im Voraus!

Ich entwickle gerade ein Multiplayer-Spiel, bei welchem die Spieler sich in einer Lobby treffen und beim Start des Spiels dann in die eigentliche Spielszene wechseln. Die Spieler besitzen jeweils eigene Spieler-Objekte, welche dynamisch generiert werden (basierend auf einem Prefab). Ein solches Spieler-Objekt hat eine Script-Komponente, bei welcher ich gerne gewisse andere Spiel-Objekte referenzieren möchte. Das Problem dabei ist, dass ich bei eben jener Referenzierung in gewissen Fällen eine NullPointer-Exception erhalte. Im folgenden eine Liste aus Code-Snippets, Dingen, die ich schon versucht habe und diversen Fragen.

Hier einmal die Funktion, welche die Referenzierung erledigt:
private GameObject ball;
private GameObject game;
private GameController gameScript;
private GameObject fallToDeath;
private GameObject[] playerList;

[...]

void Reference() {
	 ball = GameObject.FindGameObjectWithTag("Ball");
	 game = GameObject.FindGameObjectWithTag("Game");
	 gameScript = game.GetComponent<gamecontroller>();
	 fallToDeath = GameObject.FindGameObjectWithTag("FallToDeath");
	 playerList = GameObject.FindGameObjectsWithTag("Player");
	 Debug.LogError("Ball: " + ball);
	 Debug.LogError("Game: " + game);
	 Debug.LogError("GameScript: " + gameScript);
	 Debug.LogError("FallToDeath: " + fallToDeath);
	 Debug.LogError("PlayerList: " + playerList);
	 //Send ready command to server
	 if (!isLocalPlayer) {
		  return;
	 }
	 Debug.LogError("ClientReady");
	 CmdClientReady();
}


Normalerweise würde man die anderen Objekte ja in Awake() oder Start() referenzieren (welche der beiden Callbacks ist dabei vorzuziehen und warum?). Doch wenn ich Reference() in Awake() oder Start() aufrufe, erhalte ich eine NullPointer-Exception, wenn ich versuche die Script-Komponente auf dem Game-Objekt zu referenzieren.

void Awake() {
	 Reference()
}

--> Wenn ich den Host (Client+Server) im Editor laufen lasse, erhalte ich KEINE NullPointer-Exception. Wenn der Host im Standalone läuft schmeisst er den NullPointer bei:
gameScript = game.GetComponent<gamecontroller>();

--> Egal ob der Client im Editor oder auf dem Standalone läuft erhalte ich die NullPointer-Exception.

Start():
void Start() {
	 Reference()
	 [...]
}

--> Hier ist es egal, ob Editor oder Standalone. Der Host erhält immer die Nullpointer-Exception (an der selben Stelle), während der Client keine Probleme damit hat.


Nachdem diese beiden Ansätze zu nichts geführt hatten, habe ich versucht das Problem mithilfe einer Coroutine zu umgehen. Diese sah wie folgt aus.
IEnumerator Reference() {
int count = 0;
playerList = GameObject.FindGameObjectsWithTag("Player");
while (ball == null || game == null || fallToDeath == null || playerList.Length !=  numberOfPlayers) {
  if (ball == null) {
   ball = GameObject.FindGameObjectWithTag("Ball");
  }
  if (game == null) {
   game = GameObject.FindGameObjectWithTag("Game");
   if (game != null) {
gameScript = game.GetComponent<gamecontroller>();
   }
  }
  if (fallToDeath == null) {
   fallToDeath = GameObject.FindGameObjectWithTag("FallToDeath");
  }
  if (playerList.Length != numberOfPlayers) {
   playerList = GameObject.FindGameObjectsWithTag("Player");
  }
  yield return null;
  count++
}
Debug.LogError("Count: " + count);
//Send ready command to server
if (!isLocalPlayer) {
   return;
}
Debug.LogError("ClientReady");
CmdClientReady();
}

Diese habe ich dann in Awake() aufgerufen:
Awake():
[CODE]
void Awake() {
StartCoroutine(Reference());
}

-->Hier werden alle Objekte richtig referenziert! Interessant ist, dass count beim Host am Ende 1 war (entsprechend hat hier die Referenzierung beim ersten Aufruf bereits funktioniert) und beim Client 2. Warum auch immer, scheint der Client 2 Frames zu benötigen, bis auf die Script-Komponente des Game-Objekts zugegriffen werden kann!

Da die Initialisierung mit einer Coroutine jedoch nicht gerade elegant ist, habe ich mir überlegt, ob es nicht ein Callback geben müsste, bei welchem alle Objekte fertig geladen sind (Ist das nicht bereits in Awake() und Start() so der Fall?). Also habe ich weiter recherchiert, bis ich auf den Event SceneManager.sceneLoaded gestossen. Also fix meinen Code wieder umgeschrieben und die Methode dem Event-Listener angehängt:
void Awake() {
SceneManager.sceneLoaded += Reference;
}

[...]

void Reference(Scene scene, LoadSceneMode loadMode) {
ball = GameObject.FindGameObjectWithTag("Ball");
Debug.LogError("Ball: " + ball);
game = GameObject.FindGameObjectWithTag("Game");
Debug.LogError("Game: " + game);
gameScript = game.GetComponent<gamecontroller>();
Debug.LogError("GameScript: " + gameScript);
fallToDeath = GameObject.FindGameObjectWithTag("FallToDeath");
Debug.LogError("FallToDeath: " + fallToDeath);
playerList = GameObject.FindGameObjectsWithTag("Player");
Debug.LogError("PlayerList: " + playerList);

//Send ready command to server
if (!isLocalPlayer) {
return;
}
Debug.LogError("ClientReady");
CmdClientReady();
}

--> Es passiert gar nichts. Der Event wird NUR dann gefeuert, wenn der Host im Standalone läuft. In allen anderen Fällen wird Reference() gar nicht aufgerufen. Und WENN Referencen dann aufgerufen wird (Host im Standalone), dann erhalte ich wieder die gleiche NullPointer-Exception <_< .


-Wann weiss man, dass die Szene fertig geladen ist?
-Kann man Szenen asynchron laden in UNET? (Das habe ich nämlich auch versucht, aber progress ging nie über 0.9, obwohl ich allowChangeScene auf true gesetzt habe. Wäre noch nützlich für einen Loading-Screen)
-

Ich bin froh um jede Rückmeldung.

Mit freundlichen Grüssen,
Lynxxx

PS: Die Foren-Suche hat nicht funktioniert. Falls es das Thema bereits gibt, tut es mir leid.
PPS: Ich war mir nicht ganz sicher, ob das jetzt in Networking oder Allgemeines gehört. Falls der Post am falschen Ort ist, bitte verschieben.

#2
Sascha

    Community Manager

  • Administrators
  • 9.628 Beiträge:
  • LocationHamburg
Eine NullReferenceException in dieser Zeile kann nur auftreten, wenn game null ist.
Das heißt, das in der davorliegenden Zeile
GameObject.FindGameObjectWithTag("Game");

null zurück gibt.
Mit hoher Wahrscheinlichkeit hat das gesuchte GameObject in dem Moment nicht den richtigen Tag - oder es existiert gar nicht.

Dass man so etwas immer nur schwer diagnostizieren und reparieren kann, ist einer der Gründe, warum ich nahezu immer davon abrate, Tags zu benutzen.
Es gibt verschiedene, wesentlich vorteilhaftere Möglichkeiten, an seine Referenzen zu kommen.
In deinem Fall klingt der Name des gesuchten Objekts (game bzw. GameController) nach etwas einmaligem. Da würde sich static anbieten. Genauer das in Unity oft verwendete Pseudo-Singleton.

Dieser Code kommt in deine GameController-Klasse:
public GameController singleton { private set; get; }

void Awake()
{
  singleton = this;
}

Das ist die simpelste Form, das zu implementieren.

Statt jetzt die Referenz auf den GameController im Awake-Code von potentiell beliebig vielen Klassen herauszusuchen, wird eine statisch zugreifbare Referenz an einer zentralen Stelle gesetzt: In der Klasse selbst.

In jeder anderen Klasse kannst du an (fast) jeder Stelle im Code einfach sagen
GameController.singleton.Foo();


Und zu der Frage mit Awake oder Start: Referenzen suchen und Startwerte setzen (sowas wie GetComponent) kommt in Awake, Spiellogik (wie das Einfärben oder Bewegen eines Objekts) kommt in Start.
Scripting-Tutorials - unbedingt lesen!
BlackIceGames.de - Online Unity-Spiele
indiepowered.net - veröffentliche dein eigenes Spiel!

#3
Lynxxx

    Newbie

  • Members
  • PIP
  • 7 Beiträge:
Hallo Sascha

Danke für deine Antwort.
Der Tag stimmt und wird nie verändert, darum gehe ich davon aus, dass das Objekt noch gar nicht existiert zu diesem Zeitpunkt. ABER: Start() wird doch erst aufgerufen, NACHDEM alle Objekte Awake() abgeschlossen haben oder? Dann müssten ja alle Objekte existieren. Da das oben gezeigte Script auf dem Player ist, gehe ich davon aus, dass es etwas mit der Prefab-Initialisierung zu tun hat. Möglicherweise wird das Spieler-Prefab vor der Szene geladen, aber unabhängig von derselben.

Es kann doch nicht sein, dass die order of execution (alle Awake() vor Start()) plötzlich nicht mehr konsistent ist. Sobald man EIN Frame wartet (momentan meine Lösung mit der Coroutine und yield return null), funktioniert es!

Nun zu deinem Lösungsansatz:
Ich gebe zu, ich bin etwas skeptisch. Zwar hast du grundsätzlich recht, dass das Game-Objekt einmalig ist, aber es geht ja nicht nur um dieses Element. Da das Spieler-Objekt auf einem Prefab beruht, habe ich überhaupt keine Referenzierungen zu anderen Objekten in der Szene. Gibt es noch andere Alternativen?

Mit freundlichen Grüssen,
Lynxxx

#4
Sascha

    Community Manager

  • Administrators
  • 9.628 Beiträge:
  • LocationHamburg
Alle Awakes sollten vor allen Starts ausgeführt werden, ja. Gilt natürlich nur für Objekte, die über die geladene Szene erstellt werden. Dinge, die durch Instantiate oder Netzwerkcode hinzukommen, führen Awake und Start nach dem Frame aus, in dem sie erstellt wurden.

Wenn es nicht daran liegt, dass das game-GameObject erst nach dem Laden der Szene erstellt wird, fällt mir auch nicht mehr wirklich etwas ein.
Scripting-Tutorials - unbedingt lesen!
BlackIceGames.de - Online Unity-Spiele
indiepowered.net - veröffentliche dein eigenes Spiel!

#5
Lynxxx

    Newbie

  • Members
  • PIP
  • 7 Beiträge:
Sorry, dass ich mich erst jetzt wieder melde. Vielleicht sollte ich das noch etwas genauer beschreiben:
Die Referenzierungs-Probleme treten beim Spieler-Objekt auf, welches vom LobbyManager verwaltet wird. Das Game-Objekt besteht bereits in der Szene.
Meine Vermutung ist nun, dass das Spieler-Objekt unabhängig von der Szene geladen wird und irgendwie schneller fertig ist, als die Szene. Entsprechend sind dann bei Start() die Szenen-Objekte noch nicht geladen. Ich versuche gerade über OnLobbyServerSceneChanged() oder OnLobbyServerSceneLoadedForPlayer() die Initialisierung zu machen. Da die Dokumentation darüber dazu zu Wünschen übrig lässt, bin ich mir nicht schlüssig, ob dieser Ansatz überhaupt verlässlich ist.

Falls dies nicht funktioniert, werde ich in einem weiteren Schritt versuchen den Grossteil der Szene über das Spieler-Objekt zu initialisieren (mit Prefabs). Was meinst du dazu? Ist das eine blöde Idee?

Und (das kam mir gerade JETZT in den Sinn^^) wäre es nicht möglich ein Referenzierungs-Objekt zu erstellen, das ein Singleton ist und Referenzen zu allen relevanten, sich in der Szene befindlichen Objekte, enthält? Dann kann ich die Referenzen direkt im Editor machen (mit public Variablen) und dann auch den Spieler da hinzufügen bzw. die anderen Referenzen davon auslesen. Oder ist das zu ineffizient?

Bin gespannt auf deine Meinung : )

Mfg, Lynxxx

PS: Ich habe auch mit Spawn() ein Problem und werde dazu einen weiteren Post erstellen. Aber ich zweifle schon ein wenig an mir momentan. Es kann ja fast nicht sein, dass es einfach nicht funktioniert......Jedenfalls habe ich mir überlegt auf Photon zu wechseln ODER aber das ganze Projekt von komplett neu zu erstellen und diesmal die Lobby selber zu schreiben anstatt das von Unity bereitgestellte Asset zu verwenden. (Oder aber auf UE4 zu wechseln, wobei ich nicht glaube, dass es das besser macht^^)

#6
Sascha

    Community Manager

  • Administrators
  • 9.628 Beiträge:
  • LocationHamburg
Es würde mich schon wundern, wenn dein Netzwerk-Lade-Kram tatsächlich schneller Dinge spawnen kann als die Szene, in der es steckt. Es sei denn, die Szene wird, genau wie der Spieler, vom LobbyManager geladen.
Aber beschwören kann ich da nichts, ich arbeite nur selten mal mit Networking. Wann immer ich damit gearbeitet habe, bin ich aber nicht auf Probleme gestoßen, die sich nicht hätten lösen lassen.

Was ich an deiner Stelle erst einmal machen würde: Überall in alle relevanten Events (Start, Awake, OnConnect oder was auch immer) aller relevanten Objekte prints einfügen und sich damit die Reihenfolge, in der Dinge passieren, in der Konsole ausgeben lassen.

Ich merke auch gerade, dass ich deine letzte Frage gar nicht beantwortet habe.
Ja, es gibt verschiedene Methoden, Referenzen auf Szenenobjekte zu finden. Beim Einfügen eines Prefabs, das Objekte aus der Szene kennenlernen muss, sind statische Referenzen an zentralen Punkten meistens sehr praktisch. Bei einmaligen Objekten kann man das Pseudosingleton bauen wie oben beschrieben; bei mehrfach auftauchenden Objekten ist eine statische Liste möglich.
Die funktioniert ungefähr so:
private static List<Klassenname> instances = new List<Klassenname>();

void Awake()
{
  instances.Add(this);
}

void OnDestroy()
{
  instances.Remove(this);
}


Scripting-Tutorials - unbedingt lesen!
BlackIceGames.de - Online Unity-Spiele
indiepowered.net - veröffentliche dein eigenes Spiel!

#7
Lynxxx

    Newbie

  • Members
  • PIP
  • 7 Beiträge:
Ich habe nun versucht das Problem mit der OnLobbyServerSceneLoadedForPlayer() Funktion zu lösen. Aber, obwohl der Name es eigentlich impliziert, ist die Szene NICHT geladen, wenn der Funktionsaufruf geschieht. Im folgenden der Code:

public override bool OnLobbyServerSceneLoadedForPlayer(GameObject lobbyPlayer, GameObject gamePlayer)
	    {
		    //This hook allows you to apply state data from the lobby-player to the game-player
		    //just subclass "LobbyHook" and add it to the lobby object.

		    //Simon: Hand over information from lobby to game
		    gamePlayer.GetComponent<PlayerController1>().playerColor = lobbyPlayer.GetComponent<LobbyPlayer>().playerColor;
		    gamePlayer.GetComponent<PlayerController1>().playerName = lobbyPlayer.GetComponent<LobbyPlayer>().playerName;
		    gamePlayer.GetComponent<PlayerController1>().playerNumber = lobbyPlayer.GetComponent<LobbyPlayer>().playerNumber;
		    Debug.LogError("OnLobbyServerSceneLoadedForPlayer");
		    Debug.LogError("gamePlayer: " + gamePlayer.GetComponent<PlayerController1>().playerName);
		    gamePlayer.GetComponent<PlayerController1>().sceneLoaded = true;
		    if (_lobbyHooks)
			    _lobbyHooks.OnLobbyServerSceneLoadedForPlayer(this, lobbyPlayer, gamePlayer);
		    return true;
	    }


Die sceneLoaded Variable ist eine SyncVar auf dem Spieler-Objekt und hat einen hook auf die Reference() Funktion:

public void Reference(bool sceneLoaded) {
	    Debug.LogError("Referencing for: " + playerName);
	    Debug.LogError("IsLoaded :" + SceneManager.GetActiveScene().isLoaded);
	    Debug.LogError("IsServer: " + isServer);
	    Debug.LogError("IsLocalPlayer: " + isLocalPlayer);
	    ball = GameObject.FindGameObjectWithTag("Ball");
	    ballScript = ball.GetComponent<BallController>();
	    game = GameObject.FindGameObjectWithTag("Game");
	    gameScript = game.GetComponent<GameController>();
	    fallToDeath = GameObject.FindGameObjectWithTag("FallToDeath");
	    ability1UIScript = GameObject.FindGameObjectWithTag("CooldownAbility1").GetComponent<TextUI>();
	    ability2UIScript = GameObject.FindGameObjectWithTag("CooldownAbility2").GetComponent<TextUI>();
	    ability3UIScript = GameObject.FindGameObjectWithTag("CooldownAbility3").GetComponent<TextUI>();
	    Debug.LogError("Donered");
	    //Send ready command to server
	    //TEST IF LOCALPLAYER CHECK IS ENOUGH
	    if (isLocalPlayer) {
		    Debug.LogError("ClientReady: " + playerName);
		    CmdClientReady();
	    }
    }


Reference wird aufgerufen, aber bei der Überprüfung, ob die Szene geladen ist (mit SceneManagement.GetActiveScene().isLoaded), wird angegeben, dass die Szene noch nicht geladen ist. Dies geschieht aber nur beim ERSTEN Aufruf der Funktion. Die Funktion wird einmal pro Spieler aufgerufen, aber erst beim zweiten Spieler-Objekt (bzw. beim zweiten Spieler, der geladen hat) ist isLoaded true. Weiss jemand warum?

#8
Lynxxx

    Newbie

  • Members
  • PIP
  • 7 Beiträge:
Danke für deine schnelle Antwort Sascha.
Ich habe, wie von dir vorgeschlagen, alle Awake() und Start() mit einem Log versehen. Dabei stellte ich fest, dass meine Vermutung richtig war. Die Awake() werden mindestens ab und zu (habe da nicht mehr gross weiter getestet) nach dem Initialisieren des Spieler-Objekts erst geladen. Dies erklärt auch, warum es zur NullPointer-Exception kommt. Jetzt frage ich mich, ob es nicht einen Callback gibt, bei welchem garantiert ist, dass die Szene geladen ist. Ich dachte, dass OnLobbyServerSceneLoadedForPlayer() genau das macht, aber wie bereits erwähnt, scheint dies nicht der Fall zu sein. Ich werde noch etwas weiter forschen. Vielleicht kann ich das Spawnen des Spielers verhindern, bis die Szene fertig geladen ist. Dies würde auch das Implementieren eines Ladebildschirms vereinfachen.

Und falls das alles nichts bringt, werde ich halt mein Game-Objekt als Singleton initialisieren und für die Referenzierung missbrauchen :P.

Mfg, Lynxxx

#9
Sascha

    Community Manager

  • Administrators
  • 9.628 Beiträge:
  • LocationHamburg
Natürlich wäre es wünschenswert, genau das richtige Event zu finden und es damit zu lösen, aber ich hatte gerade so eine Idee...
Was hältst du von
private int componentsToLoad = 0;
private bool hasLoaded
{
  get { return componentsToLoad == 0; }
}

private void GetReferenceSometime<T>(ref T target, Func<T> func) where T : Object // vllt. auch nicht Object...
{
  StartCoroutine(GetReferenceSometime(target, func));
}

private IEnumerator GetReferenceSometime_Coroutine<T>(ref T target, Func<T> func) where T : Object // vllt. auch nicht Object...
{
  componentsToLoad++;
  target = func();
  while(target == null)
  {
    yield return null;
    target = func();
  }
  componentsToLoad--;
}

und dann in Awake
GetReferenceSometime(ref game, () => GameObject.FindGameObjectWithTag("Game"));


Scripting-Tutorials - unbedingt lesen!
BlackIceGames.de - Online Unity-Spiele
indiepowered.net - veröffentliche dein eigenes Spiel!

#10
Lynxxx

    Newbie

  • Members
  • PIP
  • 7 Beiträge:
Okay, folgende Idee: Jeder Client lädt das Spieler-Objekt selber. Dazu registriere ich eine Funktion auf dem Event sceneLoaded (SceneManager.sceneLoaded += OnSceneLoaded;). Diese ruft dann ClientScene.AddPlayer() auf. Auf dem, im folgenden initialisierten, Spieler-Objekt referenziere ich alles nötige und sende dem Server die Nachricht, dass der Client bereit ist.

Der Vorteil hierbei ist, dass ich sowohl weiss, wann der Client/Alle fertig geladen haben (für einen allfälligen Ladebildschirm) als auch, dass ich auch gleich verschiedene Spieler-Prefabs benutzen kann (was sowieso noch auf der Liste steht).

Als ich nun jedoch die Option "Auto Create Player" auf false gesetzt habe, ist mir aufgefallen, dass der LobbyPlayer auch nicht mehr automatisch erstellt wurde. Weisst du per Zufall, wo/wann (bzw. mit welchem Callback) ich diesen erstellen sollte? (Ich verwenden das Unity Asset für den LobbyManager).

Momentan überlege ich mir gerade, ob ich nicht die ganze Lobby über den Haufen werfen soll und alles selber schreiben möchte. Spätestens, bei der Steamworks Integration muss ich das sowieso machen oder? (Soweit ich weiss hat Unity ja noch keine Integration davon oder?).

Mit freundlichen Grüssen,
Lynxxx

#11
Sascha

    Community Manager

  • Administrators
  • 9.628 Beiträge:
  • LocationHamburg
Deine Idee klingt nicht schlecht, aber für eine vernünftige Bewertung fehlt mir die Erfahrung mit Networking.

Was Steamworks angeht - Unity bringt von sich aus nichts mit, aber es gibt gute, kostenlose Pakete. Im Asset Store und anderswo.
Scripting-Tutorials - unbedingt lesen!
BlackIceGames.de - Online Unity-Spiele
indiepowered.net - veröffentliche dein eigenes Spiel!

#12
Lynxxx

    Newbie

  • Members
  • PIP
  • 7 Beiträge:
Okay danke :). Sobald ich fertig bin poste ich meine Lösung hier.

Mfg





1 Besucher lesen dieses Thema

Mitglieder: 0, Gäste: 1, unsichtbare Mitglieder: 0