I'm trying to set up a function that updates a players sprites/colours based on a class that isn't derived from monobehaviour, but because of this it wont allow me to add it to the photonview.
My game has a class called PlayerDataClass that stores all the information for the local player. I haven't set this up as a monobehaviour class as I want to be able to access it without attaching it to a gameobject. However because of this I'm unable to access it with photonview.
Is there a way to do this? Or is there a better alternative?
I'm currently just logging the name of the ColorTheme variable attached to PlayerDataClass, but it just logs the local client variable rather than the client who called the function.
PlayerDataClass.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using Photon.Pun;
[Serializable]
public class PlayerDataClass
{
[SerializeField] private string username;
[SerializeField] private string colorTheme;
[SerializeField] private string color1;
[SerializeField] private string color2;
public string Username {
get { return username; }
set {
username = value;
PhotonNetwork.NickName = value;
}
}
public string ColorTheme {
get { return colorTheme; }
set { colorTheme = value; }
}
public string Color1 {
get { return color1; }
set { color1 = value; }
}
public string Color2 {
get { return color2; }
set { color2 = value; }
}
}
NetworkPlayer.cs
using Photon.Pun;
using Photon.Realtime;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
public class NetworkPlayer : MonoBehaviourPun
{
public Vector3 m_NetworkPosition = Vector3.zero;
Rigidbody2D rb;
public PlayerDataClass m_Player = new PlayerDataClass();
void Awake()
{
// Import scripts
}
void Start()
{
rb = GetComponent<Rigidbody2D>();
if (!photonView.IsMine)
{
photonView.RPC("UpdateColor", RpcTarget.AllBuffered, null);
}
}
[PunRPC]
void UpdateColor()
{
Debug.Log("local color name is " + m_Player.ColorTheme);
}
}
It sounds like what you want is sync the m_Player.ColorTheme to other clients.
Your NetworkPlayer which is the "owner" of the PlayerDataClass instance is a MonoBehaviourPun so you can relay all requried sync calls through it .. like you basically already did with UpdateColor ... you just need to pass in a parameter:
[PunRPC]
void UpdateColor(string theme)
{
// Assign the received string
m_Player.ColorTheme = theme;
Debug.Log($"local color name is now \"{m_Player.ColorTheme}\"");
}
and call it like
// instead you should rather do this only if you ARE the owner of this view
// YOU want to tell OTHER users what your color is
if (photonView.IsMine)
{
// Pass in the string to be synced
// Rather use OthersBuffered since you don't want to receive
// your own call again
photonView.RPC(nameof(UpdateColor), RpcTarget.OthersBuffered, m_Player.ColorTheme);
}
You could also consider to make your entire PlayerDataClass a photon synchronizable Custom Type and send it over entirely.
Related
I am new with unity and C#, I have question about how I save current scrollrect position.
Example : I am scrolling the view , and move to another scene and then back to previous scene but the scroll shows the previous position before I moved the scene, not resetting the scroll to default.
Unfortunately, what you want to make is not available ready-made, you have to make it yourself
first use Recyclable-Scroll-Rect
When scrolling to the bottom of the scroll, you have to save the id you sent to DemoCall via PlayerPrefs, then when you go to another scene and back again to the selected scene, call the scroll info from the point it left off, which is the id you saved
EDIT
After adding the Recyclable-Scroll-Rect, you can use this code
using System.Collections.Generic;
using UnityEngine;
using PolyAndCode.UI;
using System.Collections;
public struct ContactTsnif
{
public string id;
}
public class Objx
{
public string id;
}
public class RecyclTsnif : MonoBehaviour, IRecyclableScrollRectDataSource
{
[SerializeField]
RecyclableScrollRect _recycHat;
public GameObject RecyScrHat;
[SerializeField]
public int _dataLenHat;
public int beginning;
private List<ContactTsnif> _contactList = new List<ContactTsnif>();
public List<string> id = new List<string>();
void Start()
{
beginning = PlayerPrefebs.GetInt("Start", 5)// start with 5
GetHat();
}
public void GetHat()
{
_dataLenHat = 0;
_recycHat.DataSource = this;
InitDataHat();
RecyScrHat.GetComponent<RecyclableScrollRect>().Initialize();
}
public void InitDataHat()
{
if (_contactList != null) _contactList.Clear();
for (int i = beginning; i < _dataLenHat;)
{
ContactTsnif obj = new ContactTsnif();
obj.id = id[i];
i++;
_contactList.Add(obj);
}
}
#region DATA-SOURCE
public int GetItemCount()
{
return _contactList.Count;
}
public void SetCell(ICell cell, int index)
{
var item1 = cell as DemoTsnif;
item1.ConfigureCellSor(_contactList[index], index);
}
#endregion
}
Demo
using UnityEngine;
using System;
using System.Collections;
public class DemoTsnif : MonoBehaviour, ICell
{
private ContactTsnif _ContactInfo;
private int _cellIndex;
public int id;
public void GetData()
{
}
public void ConfigureCellSor(ContactTsnif contactInfo,int cellIndex)
{
_cellIndex = cellIndex;
_ContactInfo = contactInfo;
id = contactInfo.id;
GetData();
}
}
Do you tried read / write normalizedPosition?
You basically need to do two things:
You need to attach this script to the GameObject which contains the ScrollRect in order to persist the position:
using UnityEngine;
using System.Collections;
using UnityEngine.EventSystems; // Required when using event data
using UnityEngine.UI;
public class DragNotify : MonoBehaviour, IEndDragHandler // required interface when using the OnEndDrag method.
{
//Do this when the user stops dragging this UI Element.
public void OnEndDrag(PointerEventData data)
{
PlayerPrefs.SetFloat("scrollX", this.GetComponent<ScrollRect>().normalizedPosition.x);
}
}
You also need to apply the normalizedPosition once you initialized the ScrollRect and after you applied the desired content:
this.transform.Find("Scroll View").GetComponent<ScrollRect>().normalizedPosition = new Vector2(PlayerPrefs.GetFloat("scrollX"), 0F);
What I'm trying to do:
A ScriptableObject class to hold a single variable that can be subscribed to in an observer pattern to receive notifications when the value changes.
My intent is to have things like a UI display update when whatever they display changes, without having to manually trigger an event on every change.
Additionally, I want my class to have three features:
Use try/catch in order to really decouple things and not make all listeners fail just because one did
Have the option to log stuff for debugging
Show the list of currently active observers in the inspector
I thought that's a few lines of code with Delegates, but it turns out nope, that simply doesn't work.
My first naive iteration was this:
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
[CreateAssetMenu(fileName = "New Observable Float", menuName = "Observables/Float")]
public class ObservableFloat : ScriptableObject {
public event Action<float> get;
[SerializeField] private float m_Value;
public float Value {
get {
return m_Value;
}
set {
m_Value = value;
get?.Invoke(value);
}
}
}
My second iteration, which works functionally, but doesn't show me the list of observers in the inspector, was this:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
[CreateAssetMenu(fileName = "New Observable Float", menuName = "Observables/Float")]
public class ObservableFloat : ScriptableObject {
[SerializeField] List<UnityAction<float>> listeners = new List<UnityAction<float>>();
[SerializeField] private float m_Value;
public float Value {
get {
return m_Value;
}
set {
m_Value = value;
foreach (UnityAction<float> action in listeners) {
action.Invoke(value);
}
}
}
public void AddListener(UnityAction<float> func) => listeners.Add(func);
public void RemoveListener(UnityAction<float> func) => listeners.Remove(func);
}
My third iteration, replacing UnityAction with UnityEvents, appears to work at first glance (the list shows up in the Inspector), but it never updates the list and it's always shown as empty, even though again functionally it works:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using Sirenix.OdinInspector;
[CreateAssetMenu(fileName = "New Observable Float", menuName = "Observables/Float")]
public class ObservableFloat : ScriptableObject {
public UnityEvent<float> listeners = new UnityEvent<float>();
[SerializeField] private float m_Value;
public float Value {
get {
return m_Value;
}
set {
m_Value = value;
listeners?.Invoke(value);
}
}
}
In general I think what you are looking for would be UnityEvent
[SerializeField] private UnityEvent<float> listeners;
public void AddListener(Action<float> action) => listeners.AddListener(action);
public void RemoveListener(Action<float> action) => listeners.RemoveListener(action);
[SerializeField] private float m_Value;
public float Value {
get {
return m_Value;
}
set {
m_Value = value;
listeners?.Invoke(value);
}
}
Unfortunately these will always only show the persistent listeners in the Inspector. There is no simple built-in way to also display runtime callbacks, and if you want to do this I guess there is no way around Reflection and/or a very complex special Inspector implementation.
You could e.g. store something like
using System.Reflection;
using System.Linq;
...
[Serializable]
public class ListenerInfo
{
public Action<float> action;
public string MethodName;
public string TypeName;
}
[SerializeField] private List<string> listenerInfos;
public void AddListener(Action<float> action)
{
listeners.AddListener(action);
var info = action.GetMethodInfo();
listenerInfos.Add(new ListenerInfo { action = action, MethodName = info.Name, TypeName = info.DeclaringType.Name });
}
public void RemoveListener (Action<float> action)
{
listeners.RemoveListener(action);
var info = var info = action.GetMethodInfo();
listenerInfos.RemoveAll(l => l.action == action);
}
Also see e.g. Action delegate. How to get the instance that call the method
I guess that would kinda be the closest you can get without really diving deep into Unity Editor scripting and even more reflection ^^
I've come up with a solution that works, though I'm not perfectly sure about it, so I posted it in CodeReview - https://codereview.stackexchange.com/questions/272241/unity3d-observable-variable
Here's the code (but check the above link for possible fixes/improvements). A huge thanks to #derHugo who pointed me in the right direction:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEditor;
using Sirenix.OdinInspector;
[CreateAssetMenu(fileName = "New Observable Float", menuName = "Observables/Float")]
public class ObservableFloat : ScriptableObject {
[System.Serializable]
public class Listener {
[DisplayAsString, HideInInspector] public int ID;
[DisplayAsString, HideLabel, HorizontalGroup] public string Observer;
[DisplayAsString, HideLabel, HorizontalGroup] public string Method;
[HideInInspector] public UnityAction<float> Callback;
public Listener(UnityAction<float> cb) {
ID = cb.GetHashCode();
Observer = cb.Target.ToString();
Method = cb.Method.ToString();
Callback = cb;
}
}
[Delayed]
[OnValueChanged("NotifyListeners")]
[SerializeField] private float m_Value;
public float Value {
get {
return m_Value;
}
set {
m_Value = value;
NotifyListeners();
}
}
[Tooltip("Log Invoke() calls")]
[SerializeField] bool Trace;
[Tooltip("Use try/catch around Invoke() calls so events continue to other listeners even if one fails")]
[SerializeField] bool CatchExceptions;
[ListDrawerSettings(DraggableItems = false, Expanded = true, ShowIndexLabels = false, ShowPaging = false, ShowItemCount = true)]
[SerializeField] List<Listener> listeners = new List<Listener>();
void Awake() {
// clear out whenever we start - just in case some observer doesn't properly remove himself
// maybe later I'll also add persistent listeners, but for now I don't see the use case
listeners = new List<Listener>();
}
void NotifyListeners() {
foreach (Listener listener in listeners) {
if (Trace) {
Debug.Log("invoking "+listener.Observer+" / "+listener.Method+ " / value = "+m_Value);
}
if (CatchExceptions) {
try {
listener.Callback.Invoke(m_Value);
} catch (System.Exception exception) {
Debug.LogException(exception, this);
}
} else {
listener.Callback.Invoke(m_Value);
}
}
}
public void AddListener(UnityAction<float> func) { listeners.Add(new Listener(func)); }
public void RemoveListener(UnityAction<float> func) { listeners.RemoveAll(l => l.ID == func.GetHashCode()); }
}
This works and gives me the features I wanted, not sure if it's a great solution, so I'll leave the question open for better answers.
I've been stuck at this for a while. What I want is for my outline object to be instantiated at the location of my bronze Base game object and for it to destroy when the bronze base is no longer the closest to the player.
I'm willing to completely restart my bronze script if it means I can make this easier.
Thanks in advance!
Find Closest Bronze Script
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using UnityEngine;
public class FindBronze : MonoBehaviour
{
void Update()
{
FindClosestBronze();
}
void FindClosestBronze()
{
float distanceToClosestBronze = Mathf.Infinity;
Bronze closestBronze = null;
Bronze[] allBronze = GameObject.FindObjectsOfType<Bronze>();
foreach (Bronze currentBronze in allBronze)
{
float distanceToBronze = (currentBronze.transform.position - this.transform.position).sqrMagnitude;
if (distanceToBronze < distanceToClosestBronze)
{
distanceToClosestBronze = distanceToBronze;
closestBronze = currentBronze;
}
if (distanceToBronze > distanceToClosestBronze)
{
closestBronze.GetComponent<Bronze>().notSelected();
}
closestBronze.GetComponent<Bronze>().Selected();
}
}
}
Bronze (includes outline) script
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Bronze : MonoBehaviour
{
public bool isSelected = false;
public Animator anim;
[SerializeField]
public GameObject selectedBox;
public GameObject bronzeBase;
private GameObject clone;
// Update is called once per frame
void Awake()
{
clone = (GameObject)Instantiate(selectedBox, bronzeBase.transform);
}
public void Selected()
{
if (!isSelected)
{
clone = (GameObject)Instantiate(selectedBox, bronzeBase.transform);
isSelected = true;
}
else
{
Destroy(clone);
isSelected = false;
}
}
public void notSelected()
{
Destroy(selectedBox);
}
}
In the Bronze in notSelected you are destroying the prefab selectBox!
You probably rather wanted to destroy the clone instance.
Anyway I would suggest a few things that I would do different
Instead of Instantiate and Destroy all the time rather only use SetActive
Instead of using FindObjectOfType in Update store them in a HashSet event driven: Each Bronze instance registers and unregisters itself
Depends on personal taste but I would use Linq to find the closest instance
This could look somewhat like
public class Bronze : MonoBehaviour
{
// Every instance of this component registers and unregisters itself here
public static readonly HashSet<Bronze> Instances = new HashSet<Bronze>();
[Header("References")]
public Animator anim;
[SerializeField] private GameObject selectedBox;
[SerializeField] private GameObject bronzeBase;
[Header("Debugging")]
[SerializeField] bool _isSelected = false;
private GameObject clone;
// Have a property for the selection
public bool IsSelected
{
// when something reads this property return _isSelected
get => _isSelected;
// This is executed everytime someone changes the value of IsSelected
set
{
if(_isSelected == value) return;
_isSelected = value;
clone.SetActive(_isSelected);
}
}
// Update is called once per frame
void Awake()
{
// Instantiate already returns the type of the given prefab
clone = Instantiate(selectedBox, bronzeBase.transform);
// Register yourself to the alive instances
Instances.Add(this);
}
private void OnDestroy ()
{
// Remove yourself from the Instances
Instances.Remove(this);
}
}
And then use it
using System.Linq;
public class FindBronze : MonoBehaviour
{
private Bronze currentSelected;
private void Update()
{
UpdateClosestBronze();
}
void UpdateClosestBronze()
{
if(Bronze.Instances.Count ==0) return;
// This takes the instances
// orders them by distance ascending using the sqrMagnitude
// of the vector between the Instance and you
// sqrMagnitude is more efficient than Vector3.Distance when you only need to compare instead of the actual distance
// then finally it takes the first item
var newClosest = Bronze.Instances.OrderBy(b => (b.transform.position - transform.position).sqrMagnitude).First();
// skip if the result is the same as last time
if(newClosest == currentSelected) return;
// otherwise first deselect the current selection if there is one
if(currentSelected)
{
currentSelected.IsSelected = false;
}
// Then set the new selection
currentSelected = newSelected;
currentSelected.IsSelected = true;
}
}
I am trying to create a simple dialogue system for my game in Unity. I've set up a special trigger for a Dialogue to start and the code is passing right variable but somehow it gets stuck at clearing the queue and throws a NullReferenceException.
I've seen through debugger that all variables and triggers work perfectly fine till cs:27 inside DialogueManager.cs. I've also checked the inspector to make sure everything is correctly assigned.
Dialogue class:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[System.Serializable]
public class Dialogue
{
public string name;
[TextArea(3,10)]
public string[] sentences;
}
DialogueTrigger class:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class NPC_DialogTrigger : MonoBehaviour
{
// Player
public Transform player;
// GameMaager to close
public GameObject Gameplay;
// Camera and Canvas to turn on
public GameObject DialogueManager;
// NPC Details
public GameObject InteractionNPCNameTextField;
public Transform interactionTransform;
public float radius = 3f;
private bool isBusy = false;
// DialogueStart
public GameObject DialogueStart;
void Start()
{
InteractionNPCNameTextField.gameObject.SetActive(false);
}
void Update()
{
float distance = Vector3.Distance(player.position, interactionTransform.position);
if (distance <= radius)
{
if (isBusy == false)
{
InteractionNPCNameTextField.gameObject.SetActive(true);
if (Input.GetKeyDown(KeyCode.E))
{
Dialogue();
Debug.Log("Started Dialogue procedure.");
}
}
}
else if (distance > radius)
{
InteractionNPCNameTextField.gameObject.SetActive(false);
}
}
public void Dialogue()
{
Gameplay.SetActive(false);
DialogueManager.SetActive(true);
DialogueStart.GetComponent<DialogueStart>().TriggerDialogue();
Debug.Log("Triggered Dialogue.");
}
void OnDrawGizmosSelected()
{
if (interactionTransform == null)
{
interactionTransform = transform;
}
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(interactionTransform.position, radius);
}
}
DialogueStart class:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class DialogueStart : MonoBehaviour
{
public Dialogue dialogue;
public void TriggerDialogue()
{
FindObjectOfType<DialogueManager>().StartDialogue(dialogue);
Debug.Log("Dialogue sent to dialogue manager.");
}
}
DialogueManager class:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
public class DialogueManager : MonoBehaviour
{
public Text nameText;
public Text DialogueText;
private Queue<string> sentences;
public GameObject DialogueManagerUI;
void Start()
{
if (sentences == null)
{
sentences = new Queue<string>();
}
}
public void StartDialogue (Dialogue dialogue)
{
Debug.Log("Received dialogues: " + dialogue);
nameText.text = dialogue.name;
Debug.Log("Start Dialogue: " + sentences.Count);
sentences.Clear();
Debug.Log("Sentences Cleared: " + sentences);
foreach (string sentence in dialogue.sentences)
{
sentences.Enqueue(sentence);
}
DisplayNextSentence();
}
public void DisplayNextSentence()
{
if(sentences.Count == 0)
{
EndDialogue();
return;
}
string sentence = sentences.Dequeue();
DialogueText.text = sentence;
}
void EndDialogue()
{
Debug.Log("End of conversation.");
}
private void Update()
{
if (DialogueManagerUI.activeInHierarchy)
{
Cursor.lockState = CursorLockMode.None;
Cursor.visible = true;
}
else if (!DialogueManagerUI.activeInHierarchy)
{
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
}
}
}
Visual Studio doesn't give me any errors.
Unity error code -> Screenshot
Debugger right before issue -> Screenshot2
Seems to me like the Queue is never assigned to sentences and it remains empty.
If it is so - why?
It sounds like DialogueManager.StartDialogue probably via DialogueStart.TriggerDialogue is called from somewhere in either Awake or at least before your Start was executed.
Especially
DialogueManager.SetActive(true);
lets assume the DialogueManager object is inactive at first anyway. So maybe your stuff is called before it is set to active.
This might also be due to the Start where you set
InteractionNPCNameTextField.gameObject.SetActive(false);
so any component on this GameObject might not have its Start method getting called. Maybe you referenced the wrong GameObject here?
Debugging would help to figure out in which order your methods get called.
In general my thumb rule for initializing is
Do everything that depends only on yourself in Awake
Do everything that depends on other components being setup already in Start
This way you can (almost) always be sure that stuff is already initialized when you need it.
However, these are only guesses but
The simple solution here:
You could already solve this by simply initializing the sentences right away using
private readonly Queue<string> sentences = new Queue<string>();
now it is definitely initialized even before Start or Awake get called!
I've either been starting at things for too long (highly likely) or something weird is going on with a particular property. When I debug the property _animalType from my PlayerController script Debug.Log("Animal Type is : " + _animalType); it's coming back as an empty string, yet when I debug it from CanvasController Debug.Log(player.GetComponent<PlayerController>().AnimalType); it shows that the string is populated with the correct data. Can anyone see what might be happening here? Thank you!
'CanvasController.cs'
public void PrepareAnimalData()
{
StartCoroutine(DoPrepareAnimalData());
Debug.Log("Send To Forest Button Pressed.");
}
IEnumerator DoPrepareAnimalData()
{
yield return new WaitForEndOfFrame();
RenderTexture tmp = RenderTexture.active;
RenderTexture.active = BackLayerController.RenderTexture;
TmpTexture2D.ReadPixels(new Rect(0, 0, RenderTextureSize.x, RenderTextureSize.y), 0, 0, false);
player = GameObject.FindGameObjectWithTag("Player");
PlayerController.animalTex = TmpTexture2D; // set PlayerController animalTex to TmpTexture2D
player.GetComponent<PlayerController>().TexToBytes(); // run function from PlayerController
player.GetComponent<PlayerController>().AnimalType = PageConfig.UniqueId; // set animalType in PlayerController
Debug.Log(PageConfig.UniqueId);
Debug.Log(player.GetComponent<PlayerController>().AnimalType);
}}
PlayerController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
public class PlayerController : NetworkBehaviour
{
public static Texture2D animalTex;
private byte[] textureBytes;
private string _animalType;
public string AnimalType
{
get
{
return _animalType;
}
set
{
_animalType = value;
}
}
private string _playerID;
public string PlayerID
{
get
{
return _playerID;
}
set
{
_playerID = value;
}
}
private void Awake()
{
GameObject gm = GameObject.FindGameObjectWithTag("GameManager");
}
void Update()
{
if (!isLocalPlayer)
return;
}
public void TexToBytes()
{
textureBytes = animalTex.GetRawTextureData();
DebugAnimalData();
}
public void DebugAnimalData()
{
Debug.Log("Byte Array Length is : " + textureBytes.Length);
Debug.Log("Animal Type is : " + _animalType);
Debug.Log("Player ID is : " + _playerID);
}
}
You called this (which shows it outputs the info)
player.GetComponent<PlayerController>().TexToBytes();
before you set it
player.GetComponent<PlayerController>().AnimalType = PageConfig.UniqueId; // set animalType in PlayerController
When TexToBytes is called, it internally invokes DebugAnimalData logging data. At this time, _animalType is still initialized to null. However, after this, your next statement is setting _animalType through Setter and thus, next Debug logs start logging the value