I'm aware you can't pass gameObject references through RPC calls since every client uses it's own unique references for every instanciation. Is there a way to refer to a specific gameObject on ALL clients via an RPC call?
Here's a sample of what i'm trying to accomplish
if(Input.GetKeyDown("r")){
PhotonView.RPC("ChangeReady", RPCTarget.AllBuffered, gameObject)
}
[PunRPC]
void ChangeReady(GameObject PLAYER) {
PlayerScript PSCRIPT = PLAYER.GetComponent<PlayerScript>();
if (PSCRIPT.READY)
{
PSCRIPT.GetComponent<PlayerScript>().READY = false;
}
else {
PSCRIPT.READY = true;
}
}
I am trying to make a ready check system for players so they can toggle ready on and off and the information is transmitted to the other clients. Not sure how to refer to the specific gameObject since as previously mentioned it's impossible to do in RPC.
How would I go about doing this?
There are a few ways of doing this, if all you want to do is set a ready state:
a) Use CustomProperties, which allows you to store player specific data in a key-value pairs. and then, you can locally check for these values on your player(s) GameObjects and change their behaviours with your RPC. here is an Example:
bool ready = (bool)PhotonNetwork.player.customProperties["Ready"];
Hashtable hash = new Hashtable();
hash.Add("Ready", true);
PhotonNetwork.player.SetCustomProperties(hash);
Follow this link for info on CustomProperties
b) Use OnPhotonInstantiate by implementing IPunMagicInstantiateCallback On your Monobehaviour
void OnPhotonInstantiate(PhotonMessageInfo info)
{
// e.g. store this gameobject as this player's charater in Player.TagObject
info.sender.TagObject = this.GameObject;
}
the TagObject helps you associate a GameObject with a particular player. So once they are instantiated, This is called automatically, and Your GameObject can be tied to the player and retrieved using Player.TagObject and Execute your ready state logic (Via RPC or any other way)
Follow this link for info about OnPhotonInstantiate
See Photon RPC and RaiseEvent => "You can add multiple parameters provided PUN can serialize them" -> See Photon - Supported Types for Serialization
=> No it is not directly possible.
Solution
You will always need a way to tell other devices which object you are referring to since they don't have the same refernences as you.
If you are referring to objetcs of your Photon Players, though you can instead send over the
PhotonNetwork.LocalPlayer.ActorNumber
and on the other side get its according GameObject via the ActorNumber.
There are multiple ways to do so.
I once did this by having a central class like
public class PhotonUserManager : IInRoomCallbacks, IMatchmakingCallbacks
{
private static PhotonUserManager _instance;
private readonly Dictionary<int, GameObject> _playerObjects = new Dictionary<int, GameObject>();
public static bool TryGetPlayerObject(int actorNumber, out GameObject gameObject)
{
return _instance._playerObjects.TryGetValue(actorNumber, out gameObject);
}
[RuntimeInitializeOnLoadMethod]
private static void Init()
{
_instance = new PhotonUserManager();
PhotonNetwork.AddCallbackTarget(_instance);
}
public void OnCreateRoomFailed(short returnCode, string message){ }
public void OnJoinRoomFailed(short returnCode, string message){ }
public void OnCreatedRoom() { }
public void OnJoinedRoom()
{
Refresh();
}
public void OnLeftRoom()
{
_playerObjects.Clear();
}
public void OnJoinRandomFailed(short returnCode, string message){ }
public void OnFriendListUpdate(List<FriendInfo> friendList) { }
public void OnPlayerEnteredRoom(Player newPlayer)
{
Refresh();
}
public void OnPlayerLeftRoom(Player otherPlayer)
{
if(_playerObjects.ContainsKey(otherPlayer.ActorNumber))
{
_playerObjects.Remove(otherPlayer.ActorNumber);
}
}
public void OnRoomPropertiesUpdate(Hashtable propertiesThatChanged){ }
public void OnPlayerPropertiesUpdate(Player targetPlayer, Hashtable changedProps){ }
public void OnMasterClientSwitched(Player newMasterClient){ }
private void Refresh()
{
var photonViews = UnityEngine.Object.FindObjectsOfType<PhotonView>();
foreach (var view in photonViews)
{
var player = view.owner;
//Objects in the scene don't have an owner, its means view.owner will be null => skip
if(player == null) continue;
// we already know this player's object -> nothing to do
if(_playerObjects.ContainsKey(player.ActorNumber)) continue;
var playerObject = view.gameObject;
_playerObjects[player.ActorNumber] = playerObject;
}
}
}
This automatically collects the GameObject owned by the joined players. There is of course one limitation: If the players spawn anything themselves and own it .. it might fail since then there are multiple PhotonViews per player => in that case use the one with the lowest NetworkIdendity. In Photon the viewIDs are composed of first digit = ActorNumber + 3 digits ids ;)
and later do
if(!PhotonUserManager.TryGetPlayerObject(out var obj))
{
Debug.LogError($"Couldn't get object for actor number {receivedActorNumber}");
}
As another option you can use OnPhotonInstantiate
public class SomeComponentOnYourPlayerPrefab : PunBehaviour
{
public override void OnPhotonInstantiate(PhotonMessageInfo info)
{
// only set if not already set in order to not
// overwrite the tag for a player that spawns something later
if(info.sender.TagObject == null)
{
info.sender.TagObject = gameObject;
}
}
}
and then later do
var obj = PhotonNetwork.LocalPlayer.Get(receivedActorNumber).TagObject;
Related
Salutations,
I'm trying to create a script in C# that can allow the player to open and close doors and drawers. To save myself any extra work, I've attempted to kill two birds with one stone by deciding to tag both doors and drawers as "Door", as well as using the same script on them. Since both share the same script, I decided to identify them through the use of a private integer, configurable in the Unity inspector, in order to differentiate between the two. I'm trying to make the functionality different depending on the integer ID, and if the door/drawer is open or not. I don't want doors to behave like drawers, and vice versa, so I thought providing an integer value would be beneficial if I'm going to use the same script on both objects.
I made sure both objects have this script and are both tagged as "Door". Both have their own integer values set in the inspector at the "Door ID" field. I named my door object "Office Door" with a value of 0 (for door objects), and named my drawer object as "Drawer" with a value of 1 (for drawer objects).
My problem, however, is that while this script works for my door named "Office Door" object, it's not working for my drawer object named "Drawer". For one, my drawer object thinks it's named "Office Door", despite having named it "Drawer" in the inspector. Additionally, while checking Debug.Log() at playtime, I can see that the OpenDoor()/CloseDoor() functions are being called on the drawer and not OpenDrawer()/CloseDrawer(), so it still thinks it's a door.
Apologies for my messy code,
Door.cs:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Door : MonoBehaviour
{
// Door Identifier
// 0 is swing door (regular)
// 1 is pull-out drawer
[SerializeField] int _doorID = 0;
private float _drawerSpeed = 1f; // Currently unused
[SerializeField] private string doorName = "Door";
private PlayerInteractiveBubble _playerBubble;
// Boolean variables
private bool _iAmOpen = false;
void Start() {
_playerBubble = GameObject.Find("Player/InteractiveBubble").GetComponent<PlayerInteractiveBubble>();
}
void Update() {
}
public void ManageDoor() {
if (_iAmOpen) {
if (_doorID == 0) {
CloseDoor();
} else if (_doorID == 1) {
CloseDrawer();
}
} else if (!_iAmOpen) {
if (_doorID == 0) {
OpenDoor();
} else if (_doorID == 1) {
OpenDrawer();
}
}
}
private void OpenDoor() {
if (_playerBubble.doorIsInRange) {
_iAmOpen = true;
Debug.Log("Door: " + doorName.ToString() + " is open.");
}
}
private void CloseDoor() {
if (_playerBubble.doorIsInRange) {
_iAmOpen = false;
Debug.Log("Door: " + doorName.ToString() + " is now closed.");
}
}
private void OpenDrawer() {
if (_playerBubble.doorIsInRange) {
// Todo: add new Vector3, plus 0.26f on Z-axis
_iAmOpen = true;
Debug.Log("Drawer: " + doorName.ToString() + " is open.");
}
}
private void CloseDrawer() {
if (_playerBubble.doorIsInRange) {
// Todo: add new Vector3, minus 0.26f on Z-axis
_iAmOpen = false;
Debug.Log("Drawer: " + doorName.ToString() + " is closed.");
}
}
}
My function gets called inside the Update() function of PlayerController.cs:
if (Input.GetButtonDown("Open")) {
_door.ManageDoor();
}
Here is where I'm checking my collision,
PlayerInteractiveBubble.cs:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerInteractiveBubble : MonoBehaviour
{
public bool doorIsInRange = false;
// Currently Unused
private PlayerController _playerController;
private UIManager _uiManager;
void Start() {
_playerController = GameObject.Find("Player").GetComponent<PlayerController>(); // Currently Unused
}
void Update() {
}
void OnTriggerEnter(Collider other) {
if (other.gameObject.CompareTag("Door")) {
doorIsInRange = true;
}
}
void OnTriggerExit(Collider other) {
if (other.gameObject.CompareTag("Door")) {
doorIsInRange = false;
}
}
}
Thank you for taking the time to look at my problem and my code. I appreciate it.
You seem to only check if any door is in range and then open/close whatever is currently assigned to _door. And that simply seems to not be what you expect it to be.
You should not only check
if(_playerBubble.doorIsInRange)
but actually rather store the door that is in range
public Door doorInRange;
void OnTriggerEnter(Collider other)
{
// There is no need for a Tag at all!
// Rather simply check if the object contains a Door (or derived) component
if (other.TryGetComponent<Door>(out var door))
{
doorInRange = door;
}
}
void OnTriggerExit(Collider other)
{
if (other.gameObject.TryGetCompoment<Door>(out var _))
{
doorInRange = null;
}
}
and then only in the PlayerController script rather do
if (Input.GetButtonDown("Open"))
{
// In this case this is rather the implicit bool conversion
// and basically equals a check for != null
if(_playerBubble.doorInRange)
{
_playerBubble.doorInRange.ManageDoor();
}
}
The doors themselves don't need to double check whether they are in range.
And then personally I'd not use that int to differ between door types. Rather use inheritance and have your base class Door
public class Door : MonoBehaviour
{
private bool _iAmOpen = false;
public void ManageDoor ()
{
// Invert the flag
_iAmOpen = !_iAmOpen;
if(_iAmOpen)
{
// simply log the name of this door object, no need for an additional field
// by also including a context you can simply click once
// on the logged message in the console and this object will be highlighted
Debug.Log($"{name} is opened", this);
Open();
}
else
{
Debug.Log($"{name} is closed", this);
Close();
}
}
protected virtual void Open()
{
// Whatever a default door does
}
protected virtual void Close()
{
// Whatever a default door does
}
}
and then rather extend and modify the behavior slightly
public class Drawer : Door
{
public float drawerSpeed = 1f;
protected override void Open()
{
// Do different things for a drawer
// if you want to also include the default stuff additionally include a call to
//base.Open();
}
protected override void Close()
{
// Do different things for a drawer
// if you want to also do the default stuff additionally include a call to
//base.Close();
}
}
Is there a listener event to detect if an object is set to inactive from active state or to active from inactive state? I do not want to add it in Update as there will be multiple calls and it would affect my game's performance. So is there an alternative for this?
public GameObject Go_1;
public GameObject Go_2;
void Update () {
if (Go_1.activeSelf) {
} else if (Go_2.activeSelf) {
}
}
You could implement something using the Update method like e.g.
public class ActiveSelfWatcher : MonoBehaviour
{
private Dictionary<ActiveSelfProvider, bool> _lastActiveSelfState = new Dictionary<ActiveSelfProvider, bool>();
private void OnObjectBecameActive(GameObject obj)
{
Debug.Log($"{obj.name} became active!", this);
}
private void OnObjectBecameInactive(GameObject obj)
{
Debug.Log($"{obj.name} became inactive!", this);
}
private void Update()
{
// Iterate through all registered instances of ActiveSelfProvider
foreach(var provider in ActiveSelfProvider.Instances)
{
// pre-cache the GameObject reference
var obj = provider.gameObject;
// pre-cache the current activeSelf state
var currentActive = obj.activeSelf;
if(!_lastActiveSelfState.TryGetValue(provider))
{
// we don't know this provider until now
// TODO here you have to decide whether you want to call the events now once for this provider or not
// TODO otherwise it is only called for providers you already know and changed their state
}
if(currentActive != _lastActiveSelfState[provider])
{
// the state is not the one we stored for this instance
// => it changed its states since the last frame
// Call the according "event"
if(currentActive)
{
OnObjectBecameActive(obj);
}
else
{
OnObjectBecameInactive(obj);
}
}
// store the current value
_lastActiveSelfState[provider] = currentActive;
}
}
}
This is your watcher class you currently already have anyway.
Then on all the objects you want to be able to watch you use
public class ActiveSelfProvider : MonoBehaviour
{
private static readonly HashSet<ActiveSelfProvider> instances = new HashSet<ActiveSelfProvider>();
public static HashSet<ActiveSelfProvider> Instances => new HashSet<ActiveSelfProvider>(instances);
private void Awake()
{
// register your self to the existing instances
instances.Add(this);
}
private void OnDestroy()
{
// remove yourself from the existing instances
instances.Remove(this);
}
}
If this is "efficient enough" for you use case you would have to test ;)
If you want to go super fancy, someone once made a Transform Interceptor .. a quite nasty hack which on compile time overrules parts of the Unity built-in Transform property setters to hook in callbacks.
One probably could create something like this also for SetActive ;)
Note: Typed on smartphone but I hope the idea gets clear
Yes, try with OnEnable and OnDisable.
// Implement OnDisable and OnEnable script functions.
// These functions will be called when the attached GameObject
// is toggled.
// This example also supports the Editor. The Update function
// will be called, for example, when the position of the
// GameObject is changed.
using UnityEngine;
[ExecuteInEditMode]
public class PrintOnOff : MonoBehaviour
{
void OnDisable()
{
Debug.Log("PrintOnDisable: script was disabled");
}
void OnEnable()
{
Debug.Log("PrintOnEnable: script was enabled");
}
void Update()
{
#if UNITY_EDITOR
Debug.Log("Editor causes this Update");
#endif
}
}
I'm really struggling with creating a weapon/item unlock system via an in-game shop. The player should only be able to select weapons at indexes that have been unlocked or bought.
I've wasted two days on the best way to achieve this and I'm still not anywhere near a solution.
Currently this is what I have:
I have a WeaponUnlock script that handles a dictionary of weapons and another WeaponSwitcher script that access vales from that script to check if it is unlocked or not.
public class WeaponUnlockSystem : MonoBehaviour
{
private Dictionary<string, bool> weaponUnlockState;
void Start()
{
// Seeding with some data for example purposes
weaponUnlockState = new Dictionary<string, bool>();
weaponUnlockState.Add("Shotgun", true);
weaponUnlockState.Add("Rifle", true);
weaponUnlockState.Add("FireballGun", false);
weaponUnlockState.Add("LaserGun", false);
weaponUnlockState.Add("FlamethrowerGun", false);
weaponUnlockState.Add("SuperGun", false);
}
public bool IsWeaponUnlocked(string weapon_)
{
if (weaponUnlockState.ContainsKey(weapon_))
return weaponUnlockState[weapon_]; // this will return either true or false if the weapon is unlocked or not
else
return false;
}
public void UnlockWeapon(string weapon_) //this will be called by the button..
{
if (weaponUnlockState.ContainsKey(weapon_))
weaponUnlockState[weapon_] = true;
}
}
The WeaponSwitchergets the name of the weapon from the nested children every time the nextWeapon button is pressed:
weaponName = this.gameObject.transform.GetChild(weaponIdx).name.ToString(); //get the name of the child at current index
if (weaponUnlock.IsWeaponUnlocked(weaponName)) //ask weaponUnlockSystem if this name weapon is unlocked or not, only enable the transform if true is returned
selectedWeapon = weaponIdx;
This...... works..... but I know is not practical. As the script sits on a game object and at every time it is called the Start() is called that will reset all the unlocked values. Also will I have to make it persistent via DontDestroyOnLOad() through scenes?
I can get use PlayerPrefs and set a value for every weapon, but that is also not ideal and also to avoid it being reset every time someone opens my game I would have to perform a checks of PlayerPrefs.HasKey() for every weapon. Also not practical.
There must be a better way of doing this. Its funny that I cannot find much help online for this and dont know how everyone is getting around this.
Thanks
You create a class that is not a GameObject, just a normal class. There you can initialize the dictionary and have methods to unlock or check if a weapon is unlocked.
As I don't know what your architecture of the rest of the code looks like, I will go ahead and assume that you have some kind of a persistent player model.
You could just add the WeaponUnlockSystem to your player GameObject and manage the unlocks from there.
To me this would make the most sense as the player is probably the one unlocking the weapons so the WeaponUnlockSystem should be attached to him.
Here is a really basic code example:
public class WeaponUnlockSystem {
private Dictionary<string, bool> weaponUnlockState;
public WeaponUnlockSystem()
{
weaponUnlockState = new Dictionary<string, bool>();
weaponUnlockState.Add("Shotgun", true);
weaponUnlockState.Add("Rifle", true);
weaponUnlockState.Add("FireballGun", false);
weaponUnlockState.Add("LaserGun", false);
weaponUnlockState.Add("FlamethrowerGun", false);
weaponUnlockState.Add("SuperGun", false);
}
public bool IsWeaponUnlocked(string weapon)
{
if (weaponUnlockState.ContainsKey(weapon))
return weaponUnlockState[weapon];
else
return false;
}
public void UnlockWeapon(string weapon)
{
if (weaponUnlockState.ContainsKey(weapon))
weaponUnlockState[weapon] = true;
}
}
public class Player : MonoBehaviour {
private WeaponUnlockSystem weaponUnlockSystem;
void Start()
{
weaponUnlockSystem = new WeaponUnlockSystem();
...
}
public void NextWeapon(string weapon)
{
...
}
}
I would also advice against using a static class for convenience here. Static should only be used, if you don't want to change the state of an object or don't need to create an instance of an object.
I had a Unity project recently where we needed to hold and manipulate some simple data between scenes. We just made a static class that doesn't extend monobehaviour.
Also, if you're looking for efficiency, why not just maintain an array of unlocked weapons?
All in all, why not try something like this:
public static class WeaponUnlockSystem
{
private static string[] unlockedWeapons = InitialWeapons();
private static string[] InitialWeapons(){
string w = new string[]{
"Shotgun",
"Rifle",
}
}
public static bool IsWeaponUnlocked(string name)
{
int i = 0;
bool found = false;
while(i < unlockedWeapons.Length && !found){
if (string.Equals(unlockedWeapons[i],name)){
found = true;
}
i++;
}
return found;
}
public static void UnlockWeapon(string name)
{
string[] weapons = new string[unlockedWeapons.Length+1];
int i = 0;
bool found = false;
while(i < unlockedWeapons.Length && !found){
if (string.Equals(unlockedWeapons[i],name)){
found = true;
} else {
weapons[i] = unlockedWeapons[i];
}
}
if(!found){
weapons[unlockedWeapons.Length] = name;
unlockedWeapons = weapons;
}
}
I'm not running my c# IDE, so I apologise if there's any syntax errors. Should be efficient, simple and - as it's static - you shouldn't need to put it on an empty GameObject to have it work.
I have TCP client (Unity c#) and server (WinForms app c#). I need my server sending some JSON commands, like this:
{ ""ObjName"": ""Cube_2"", ""Method"":""MoveLeft"", ""Delay"":0}
This certain command says to find GameObject "Cube_2" and fire method "MoveLeft".
When i recieve this from server, i convert it into my AOSCommand class:
public class AOSCommand
{
public string ObjName;
public string Method;
public int delay;
}
And then i do the following (which i think is not the best solution, so here is a question):
private void ProcessCommand(AOSCommand command)
{
GameObject cube = GameObject.Find(command.ObjName);
MonoBehaviour script = cube.GetComponent(command.ObjName.Split(new string[] {"_"}, StringSplitOptions.None)[0]) as MonoBehaviour;
script.Invoke(command.Method, command.delay);
}
How can i fire some method from AOSCommand.Method string in a better way?
The script attached to Cube_2 (and Cube_1 and may be attached to unknown count of other objects):
using UnityEngine;
public class Cube : MonoBehaviour {
private GameObject thisObj;
private void Start()
{
thisObj = this.gameObject;
}
public void MoveLeft()
{
thisObj.transform.Translate(new Vector3(1,0,0));
}
public void MoveRight()
{
thisObj.transform.Translate(new Vector3(-1, 0, 0));
}
}
It depends what you consider wrong.
You should have a single script that takes care of the parsing of the incoming data, this would remove the need to search for a component, it would always be the same.
Then you can have a dictionary of to replace the invoke call.
So your snippet turns into:
private void ProcessCommand(AOSCommand command)
{
GameObject cube = GameObject.Find(command.ObjName);
AOSDispatch dispatch = cube.GetComponent<AOSDispatch>()
if(dispatch == null){ return; } // or debug or exception
dispatch.Call(command);
}
this is on the main receiver. Then comes the script on the cubes:
public class AOSDispatch : MonoBehaviour
{
Dictionary<string, Action> dict;
void Start()
{
dict.Add("MoveLeft", MoveLeft);
dict.Add("MoveRight", MoveRight);
}
public void Call(AOSCommand command)
{
if(dict.Contains(command.Method) == false){ return; } //Or debug
// use the delay as well as you wish
dict[command.Method]();
}
private void MoveLeft(){}
private void MoveRight(){}
}
This is not necessarily better, just my two cents on it.
EDIT: there was comment mentioning the json could contain the script type to know what script to use. I would not go this way. AOSDispatch will take care of the dispatching of the message.
Message says MoveLeft, AOSDispatch can either treat the info or forward to a movement controller:
public class AOSDispatch : MonoBehaviour
{
[SerializeField] private MoveController moveCtrl = null;
Dictionary<string, Action> dict;
void Start()
{
dict.Add("MoveLeft", this.moveCtrl.MoveLeft);
dict.Add("MoveRight", this.moveCtrl.MoveRight);
}
public void Call(AOSCommand command)
{
if(dict.Contains(command.Method) == false){ return; } //Or debug
// use the delay as well as you wish
dict[command.Method]();
}
}
public class MoveController: MonoBehaviour
{
private void MoveLeft(){}
private void MoveRight(){}
}
there you go, message is forward and cleanly, the AOSDispatch does only the job it is meant to do, dispatch the AOS.
SECONDARY EDIT:
On second thought, here is an improved version.
Create a DispatchManager game object and add the following script:
public class AOSDispatch:MonoBehaviour
{
private IDictionary<string, AOSController> dict;
void Awake(){
this.dict = new Dictionary<string, AOSController>();
AOSController.RaiseCreation += ProcessCreation;
AOSController.RaiseDestruction += ProcessDestruction;
}
void OnDestroy()
{
AOSController.RaiseCreation -= ProcessCreation;
AOSController.RaiseDestruction -= ProcessDestruction;
}
private void ProcessCreation(AOSController controller){
this.dict.Add(controller.name, controller);
}
private void ProcessDestruction(AOSController controller){
AOSController temp= null;
if(this.dict.TryGetValue(controller.name, out temp) == true){
this.dict.Remove(name);
}
}
private void ProcessCommand(AOSCommand command)
{
AOSController controller = null;
if(this.dict.TryGetValue(command.ObjName, out controller) == true){
controller.Call(command);
return;
}
}
}
and then on the objects you have the AOSController that forwards the info as before (just renaming):
public class AOSController: MonoBehaviour
{
public static event Action<AOSController> RaiseCreation;
public static event Action<AOSController> RaiseDestruction;
[SerializeField] private MoveController moveCtrl = null;
Dictionary<string, Action> dict;
void Start()
{
if(RaiseCreation != null) { RaiseCreation(this); }
dict.Add("MoveLeft", this.moveCtrl.MoveLeft);
dict.Add("MoveRight", this.moveCtrl.MoveRight);
}
void OnDestroy()
{
if(RaiseDestruction != null) { RaiseDestruction(this); }
}
public void Call(AOSCommand command)
{
if(dict.Contains(command.Method) == false){ return; } //Or debug
// use the delay as well as you wish
dict[command.Method]();
}
}
public class MoveController: MonoBehaviour
{
private void MoveLeft(){}
private void MoveRight(){}
}
On Awake, the Dispatch registers to the static event from the AOSController. In the AOSController.Start, the object triggers the event and passes itself to the AOSDispatch. That one adds it to the dictionary. On destruction, the AOSDispatch gets the event and removes the AOSController.
Now you have a collection that at any given time contains all the AOSController in the scene.
As a result, you don't need to perform a GameObject.Find since you can get the object from the dictionary (real fast process).
I am trying to create a Custom EditorWindow (TransporterToolKit.cs) with buttons of my game's fast-travel points.
I have a GameObject (TransporterSystem.cs, a singleton, the manager):
that has a LIST of child GameObjects that are Nodes (The GameObjects fast travel points). Each node has a Serializable TransporterLocation that holds details about the actual location.
I get null object error:
NullReferenceException: Object reference not set to an instance of an
object
whether I am running the game or not using my current TransporterToolKit.cs file
How do I access the list of nodes so I can get their Serializable TransporterLocation?
EDITOW WINDOW:
What my problem question is about.
TransporterToolKit.cs
public class TransporterToolKit : EditorWindow {
[MenuItem("Project ToolKits/Transporter ToolKit")]
public static void ShowWindow() {
GetWindow<TransporterToolKit>("Transporter ToolKit");
}
public List<TransporterNode> nodes;
private void OnEnable() {
//ERROR COMES FROM THIS LINE
nodes = TransporterSystem.s_Instance.GetAllTransporterNodes();
}
private void OnGUI() {
GUILayout.Label("Transporter System", EditorStyles.boldLabel);
//Create a list of buttons with the location name
foreach (TransporterNode node in nodes) {
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
GUILayout.BeginHorizontal();
if (GUILayout.Button(node.ThisLocation.locationName)) {
//Do something
}
GUILayout.EndHorizontal();
EditorGUILayout.EndVertical(); //end section outline
}
}
}
The other classes.
TransporterSystem.cs
public class TransporterSystem : Singleton<TransporterSystem> {
[Header("Transporter Nodes")]
[SerializeField]
private List<TransporterNode> nodeList;
public List<TransporterNode> GetAllTransporterNodes() {
return nodeList;
}
}
TransporterNode.cs
public class TransporterNode : MonoBehaviour {
[SerializeField]
private TransporterLocation thisLocation;
public TransporterLocation ThisLocation {
get {
return thisLocation;
}
set {
thisLocation = value;
}
void Awake() {
ThisLocation.locationName = gameObject.name;
ThisLocation.transporterLocation = transform.position;
}
}
TransporterNode.cs
public enum TRANSPORTER_NODE_STATE {
OFFLINE,
ONLINE
}
[Serializable]
public class TransporterLocation {
public Vector3 transporterLocation;
public TRANSPORTER_NODE_STATE locationState;
public string locationName;
public TransporterLocation() {
transporterLocation = Vector3.zero;
locationName = "NOT SET";
locationState = TRANSPORTER_NODE_STATE.OFFLINE;
}
}
It looks like s_Instance is null. The problem is, that you are asking for the TransporterSystem static instance in OnEnable of the EditorWindow, however, the static instance will only be set in Awake during play mode. (At least this is what I assume after testing your code in my project).
I would fix this by actively searching for the TransporterSystem from within the editor window:
TransporterToolKit.cs
private void OnEnable()
{
TransporterSystem system = FindObjectOfType<TransporterSystem>();
if (system == null)
{
Debug.LogError("No TransporterSystem in scene.");
}
else
{
nodes = system.GetAllTransporterNodes();
}
}
Alternatively, you could also make your singleton implementation lazy, similar to this:
public class MonoSingleton
{
public static TransporterSystem instance
{
get
{
if (m_Instance == null)
m_Instance = Object.FindObjectOfType<TransporterSystem>();
if (m_Instance == null)
Debug.LogError("Unable to find TransporterSystem instance in scene.");
return m_Instance;
}
}
private static TransporterSystem m_Instance;
}
To fix the problem, that the Node is only updated in play mode:
// Reset is called when the component is added to a GameObject or when Reset is selected from the inspector.
void Reset() {
#if UNITY_EDITOR
UnityEditor.Undo.RecordObject(gameObject, "Update LocationNode");
#endif
ThisLocation.locationName = gameObject.name;
ThisLocation.transporterLocation = transform.position;
}
Instead of assigning the values in Awake, you need to do it at edit time. The simplest example would be via the Reset callback (but you could trigger it from your editor window or a special button as well). The important part is, that you want to not only set the values, but actually serialize the data to disk. This means, marking the scene as dirty. Unity recommends, to use the Undo class, to not only record an undoable action, but also set the scene as dirty. Alternatively, you can just do:
UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(gameObject.scene);
Beware, that the editor code needs to be either within a file that is placed in the Editor folder or surrounded by the compilation symbols UNITY_EDITOR.