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.
Related
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[System.Serializable]
public struct ImageInfo
{
public Texture texture;
public int width;
public int height;
}
public class ImagesData : MonoBehaviour
{
public ImageInfo[] images;
public static Vector2 AspectRatio(float width, float height)
{
Vector2 scale = Vector2.one;
}
}
This is the code for making a serialized dictionary for images which are 2D, how to I make it accept 3D models? I want it to be something like what is in the photo.
This is the code for making a serialized dictionary for images which are 2D
No it is not. What you show is a serialized array of ImageInfo.
For what you show in your image you can simply do
[Serializable]
public class ModeInfo
{
public string Id;
public GameObject Value;
}
public class ModelsDataManager : MonoBehaviour
{
public ModeInfo[] InteractionModes;
}
Or if you want it a bit further with supporting some of the dictionary functionalities
public class ModelsDataManager : MonoBehaviour
{
[SerializeField]
private List<ModeInfo> InteractionModes;
private bool TryGetInfo(string id, out ModeInfo value)
{
foreach (var info in InteractionModes)
{
if (info.Id == id)
{
value = info;
return true;
}
}
value = default;
return false;
// or same thing using Linq (needs "using System.Linq;")
//value = InteractionModes.FirstOrDefault(info => info.Id == id);
//return value != null;
}
public bool TryGetValue(string id, out GameObject value)
{
if (!TryGetInfo(id, out var info))
{
value = default;
return false;
}
// note that just like in normal dictionaries the value can still be "null" though
value = info.Value;
return true;
}
public void AddOrOverwrite(string id, GameObject value)
{
if (!TryGetInfo(id, out var info))
{
info = new ModeInfo()
{
Id = id
};
InteractionModes.Add(info);
}
info.Value = value;
}
}
If you really want to go for the entire dictionary functionality including key checks, cheaper value access without iteration
etc I would recommend to not implement this from scratch but rather use an existing solution like e.g.
Unity's own one which is hidden in the Core RP Library Package though (comes installed as dependency e.g. with UniversialRenderPipeline or HighDefinitionenderPipeline).
But this is very basic and only for serializing - has no useful Inspector drawer
azixMcAze's
Unity-SerializableDictionary (also available as a package)
Has a cool Inspector drawer which even handles duplicate key checks etc.
this one from Odin Inspector
Note though that Odin Inspector is not free
Or just go ahead and test one of the other many solutions ;)
I want to create public Scene[] levels; and manually assign my levels to the array, then loop through it and generate level selection buttons, but it won't show up in the inspector.
Is there any workaround?
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class True : MonoBehaviour
{
// Start is called before the first frame update
public static int money;
[SerializeField]
public SceneManager[] scenes;
void Start()
{
}
// Update is called once per frame
void Update()
{
}
public void nextscencefirst()
{
SceneManager.LoadScene("Level2");
money++;
}
}
SceneManager is a built-in static class to control scenes during runtime. We can go to any scene by calling LoadScene method from that class. Also SceneManager cannot be seen in Inspector since it cannot be serialized class.
A reminder: Your scenes should be listed on build settings for these to work.
There are three ways to do what you want to do:
Method 1. Create a string list that holds names of scenes
public List<string> sceneNameList = new List<string>;
Method 2. Create a list of indices of scenes that added in build settings.
public List<int> sceneBuildIndexList = new List<int>
If the names of scenes are long or somehow difficult or you want to use sceneBuildIndex value it would be good to create a class and reach scenes from it
[System.Serializable]
public class SceneData
{
public int sceneBuildIndex;
public string sceneKey;
//public void LoadScene() //Maybe
}
public List<SceneData> sceneDataList = new List<SceneData>();
public SceneData GetSceneData(string key)
{
for (int i = 0; i < sceneDataList.Count; i++)
{
if (sceneDataList[i].sceneKey == key)
{
return sceneDataList[i];
}
}
return null;
}
Method 3: In Unity most classes are created from UnityEngine.Object, so it may let you assign scene from the Inspector.
[System.Serializable]
public class SceneData
{
public UnityEngine.Object scene;
public string key;
public void LoadScene()
{
if (scene == null)
{
Debug.LogError("Scene is not assigned");
return;
}
string pathToScene = AssetDatabase.GetAssetPath(scene);
SceneManager.LoadScene(pathToScene); //If the scene is not set in BuildSettings it will throw an error.
}
}
public List<SceneData> sceneDataList = new List<SceneData>();
public SceneData GetSceneData(string key)
{
for (int i = 0; i < sceneDataList.Count; i++)
{
if (sceneDataList[i].key == key)
{
return sceneDataList[i];
}
}
return null;
}
private void Awake()
{
SceneData data = GetSceneData("Level1");
if (data != null)
{
data.LoadScene();
}
}
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);
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.
I saw this question asked some times but no one correlates to mine. I see people using the GetComponent() function but this one should work too.
First I receive a Bool value from a toggle button and assign it as a field of the object 'hydro':
hydroControl.cs
using UnityEngine;
using System.Collections;
using Assets.Code.PowerPlants;
public class hydroProwerControlPanel : MonoBehaviour {
private HydroElectric hydro;
public bool t1;
void Start ()
{
t1 = true;
}
public hydroProwerControlPanel (){
hydro = new HydroElectric();
}
public void turbine1State (bool t1) {
hydro.t1Bool = t1;
}
The I have the object where this bool change should be recognized and sent as an output, but it doesn't:
using System;
using UnityEngine;
namespace Assets.Code.PowerPlants
{
public class HydroElectric
{
public bool t1Bool;
float turbina1;
public float prod;
public HydroElectric ()
{
t1Bool = true;
prod = 0f;
}
public float ControlPanel ()
{
turbina1 = t1Bool ? 1.5F : 0;
prod = turbina1 ;
Debug.Log (prod);
return prod;
}
}
}
As requested, this is where the function ControlPanel() is called:
using System;
using UnityEngine;
using UnityEngine.UI;
using Assets.Code.PowerPlants;
namespace Assets.Code.Interfaces
{
public class PlayLevel1 : MonoBehaviour
{
private HydroElectric hydro;
public Text producao;
public PlayLevel1 ()
{
hydro = new HydroElectric();
}
public void OnGUI()
{
producao.text = hydro.ControlPanel().ToString();
}
}
}
Do you have any idea why does this Bool does not get updated? Any help appreciated.
PlayLevel1's Hyrdo and hydroProwerControlPanel's hydro object are two different instances. Change in one instance will not reflect the change in second instance both are independent instances .
The thing you want to achieve is only possible if you make HydroElectric static class with static members.
Hope it will help.