Unity C#: Modifying parameters in List of abstract elements - c#

I've got a class holding a List of classes, derived from an abstract superclass.
[System.Serializable]
public class ClassWithList: MonoBehaviour
{
[SerializeField]
private List<Element> elements;
public List<Element> Elements
{
get
{
if (elements == null)
elements= new List<Element>();
return elements;
}
}
}
[System.Serializable]
public abstract class Element: MonoBehaviour
{
[SerializeField]
protected bool isClickedInList;
public bool IsClickedInList
{
get { return isClickedInList; }
set { isClickedInList = value; }
}
}
I use a custom Editor for ClassWithList with a ReorderableList to modify the Element entities' values.
[CustomEditor(typeof(ClassWithList))]
public class Editor_ClassWithList : Editor
{
private ReorderableList elementList;
void OnEnable()
{
if (elementList== null)
elementList=
new ReorderableList(
serializedObject, serializedObject.FindProperty("elements"),
true, true, true, true);
elementList.drawElementCallback += DrawElement;
}
private void OnDisable() { elementList.drawElementCallback -= DrawElement; }
private void DrawElement(Rect r, int i, bool active, bool focused)
{
Element e = elementList.serializedProperty.GetArrayElementAtIndex(i).objectReferenceValue as Element;
e.IsClickedInList = EditorGUI.Toggle(new Rect(r.x, r.y, r.width, r.height - 1), e.IsClickedInList);
}
public override void OnInspectorGUI()
{
serializedObject.Update();
elementList.DoLayoutList();
serializedObject.ApplyModifiedProperties();
}
}
The list is correctly displayed and shows all elements. Clicking on the toggle button results in a correct change of the Elements isClickedInList value. But as soon as I press "Play" this value is overwritten as if it wouldn't be serialized.
Any ideas where things went wrong? Might the usage of serializedObject.FindProperty("elements") cause this problem?

Further testing with the provided example and comparision with my code revealed, that the issue was the GameObject being a prefab. Somehow this results in non-saving behaviour using serialized properties, although I couldn't reproduce the error.

Related

Trigger a function when array values are changed

I am adding a set of array values through inspector window. I am trying to achieve to trigger a function, when I change my array values. That is, my script should check if the new values are not equal to old values and then call this function.
I do not want to use Update as it will take more memory and processing power, so what could be the alternative?
public class SetValues : MonoBehaviour {
[Serializable]
public struct SetValues
{
public float Position;
public float Value;
}
public SetValues[] setValues;
void Function_ValuesChanged()
{
Debug.Log("The Value is changed");
//Do Something
}
}
Try MonoBehavior.OnValidate()
Example:
public class SetValues : MonoBehaviour {
[Serializable]
public struct SetValues
{
public float Position;
public float Value;
}
public SetValues[] setValues;
void OnValidate()
{
Debug.Log("The Value is changed");
//Do Something
}
}
If something happens in game and you want to notify other scripts use events. Events are lightweight and easy to implement.
In class that contain array, create property and change array value, only from property. Never directly interact with array field.
// Create Event Arguments
public class OnArrayValueChangedEventArgs : EventArgs
{
public float Position;
public float Value;
}
// Crate Event
public event EventHandler<OnArrayValueChangedEventArgs> OnArrayValueChanged;
Array[] myArray;
// Change Array value only from this property, so when you change value, event will be called
public Array[] MyArray
{
get { return myArray; }
set { myArray = value; OnArrayValueChangedEvent(this, new OnArrayValueChangedEventArgs() { Position = myArray.Position, Value = myArray.Value };}
}
From second class you should just subscribe to this event and do the thing. I will call this event from my GameManager Singleton class.
private void Awake()
{
GameManager.Instance.OnArrayValueChanged += Instance_OnArrayValueChanged;
}
private void Instance_OnArrayValueChanged(object sender, GameManager.OnArrayValueChangedEventArgs e)
{
// Do the thing
}

How do I store the same types of classes that have a different generic inside of a list?

I've been tinkering with this and I have a 'RespawnManager' that I want to use to manage my multiple 'SpawnPoint' classes with different generics but it ended up forcing me to use generics for my 'RespawnManager' which I don't want.
Let's say I had a SpawnPoint<T> class and I made a SpawnPoint<Enemy1>, SpawnPoint<Enemy2>, and SpawnPoint<Enemy3>. Is there any way I can make a list that can just manage multiple 'SpawnPoint's of any generic?
Base class:
public abstract class SpawnPoint<T> : MonoBehaviour
{
//how big the range of the spawn protection is
public int spawnProtectionRadius = 20;
public bool Occupied { get; set; }
public bool IsInSpawn(Transform target)
{
Debug.Log((target.position - transform.position).magnitude);
if ((target.position - transform.position).magnitude <= spawnProtectionRadius)
{
return true;
}
return false;
}
public abstract T Get();
}
Class that Inherits this
public class SeaMineSpawnPoint : SpawnPoint<Seamine>
{
public override Seamine Get()
{
return SeaMineObjectPool.PoolInstance.Get();
}
private void Start()
{
RespawnManager<Seamine>.respawnManager.AddSpawn(this);
}
}
Respawn manager:
public class RespawnManager<T> : MonoBehaviour where T : Component
{
public static RespawnManager<T> respawnManager;
[SerializeField]
private List<Transform> playerList;
[SerializeField]
private List<SpawnPoint<T>> spawnpoints;
private float respawnCounter;
private void Awake()
{
respawnManager = this;
}
private void Start()
{
foreach (SpawnPoint<T> sp in spawnpoints)
{
Debug.Log(sp.transform.position);
}
}
public void AddSpawn(SpawnPoint<T> spawnPoint)
{
spawnpoints.Add(spawnPoint);
}
public void RespawnSeaMines()
{
if (respawnCounter > 5)
{
respawnCounter = 0;
foreach (SpawnPoint<T> sp in spawnpoints)
{
foreach (Transform playerT in playerList)
{
if (sp.Occupied == false && !sp.IsInSpawn(playerT))
{
Component ourGameObj = sp.Get();
ourGameObj.transform.position = sp.transform.position;
ourGameObj.gameObject.SetActive(true);
sp.Occupied = true;
return;
}
}
}
}
}
private void Update()
{
respawnCounter += Time.deltaTime;
Debug.Log(respawnCounter);
RespawnSeaMines();
}
}
ObjectPool
//Class that's used for object pooling of different types.
//'T' must be a Unity component or it will error.
public abstract class ObjectPool<T> : MonoBehaviour where T : Component
{
//An object with this specific component that we use to copy.
[SerializeField]
private T prefab;
//Makes sure that only 1 coroutine runs at a time
private bool coroutineIsRunning;
//The singleton instance to our object pool.
public static ObjectPool<T> PoolInstance { get; private set; }
//A queue is used to organize plus activate and deactivate objects which
//have this component.
protected Queue<T> objects = new Queue<T>();
private void Awake()
{
//Set the instance of this pool to this class instance. Only one of these can be set.
if (PoolInstance != null)
{
throw new System.Exception("Singleton already exists. Cannot make another copy of this");
}
PoolInstance = this;
}
public T Get()
{
//If the queue happens to be empty, then add a brand new component.
if (objects.Count == 0) AddObjects(1);
//Returns the generic component and removes it from the queue.
return objects.Dequeue();
}
public void ReturnToPool(T objectToReturn)
{
//Disables the game object that the T component is attached to.
objectToReturn.gameObject.SetActive(false);
//Stores the T component in the queue.
objects.Enqueue(objectToReturn);
}
public void AddObjects(int count)
{
for (int i = 0; i < count; i++)
{
//Create a new copy of the prefab.
//The prefab is a game object with the T component attached to it.
T newObject = Instantiate(prefab);
//Disable the game object.
newObject.gameObject.SetActive(false);
//Add the T component to the queue.
//The T component is attached to the game object we created earlier.
objects.Enqueue(newObject);
}
}
public T GetWithDelay(int time)
{
T genericToReturn = null;
if (!coroutineIsRunning)
{
coroutineIsRunning = true;
StartCoroutine(GetCoroutine(time, genericToReturn));
}
return genericToReturn;
}
private IEnumerator GetCoroutine(int time, T generic)
{
float counter = 0;
while (counter < time)
{
counter += Time.deltaTime;
yield return null;
}
generic = Get();
generic.gameObject.SetActive(true);
coroutineIsRunning = false;
}
}
You should be able to declare your spawnpoints property in RespawnManager as a List<SpawnPoint<Component>> instead of List<SpawnPoint<T>>. That will allow you to get rid of the <T> type parameter entirely from RespawnManager and make it non-generic.

List array unity

I have a ObjectController script that looking for game objects and adding them to an array and another script that draws outline around those game objects. There're few tasks that I'm trying to achieve:
From ObjectController script List array I want to check what object is currently selected (clicked) so i won't be able to select (click) on other objects.
onMouseButtonDown(1) i want to clear selections (outline) from those objects.
Can you please guide me in the right direction?
I'm new to coding so please, go easy on me :D
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
public class ObjectController : MonoBehaviour
{
public List<SpriteOutline> objects;
private void Start()
{
List<SpriteOutline> objectList = FindObjectsOfType<SpriteOutline>().ToList<SpriteOutline>();
for (int i = 0; i < objectList.Count; i++)
{
objects.Add(objectList[i]);
}
}
public List<SpriteOutline> GetList()
{
return objects;
}
}
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;
public class SpriteOutline : MonoBehaviour {
public Color OutlineColor = Color.white;
private Color _currentColor = Color.clear;
[Range(0, 16)]
public int outlineSize = 1;
private bool clicked = false;
private SpriteRenderer spriteRenderer;
ObjectController objController;
IEnumerator Start()
{
objController = GameObject.Find("ObjectController").GetComponent<ObjectController>();
yield return new WaitForEndOfFrame();
for (int i = 0; i < objController.GetList().Count; i++)
{
Debug.Log(i);
}
}
void OnEnable() {
spriteRenderer = GetComponent<SpriteRenderer>();
UpdateOutline(true);
}
void OnDisable() {
UpdateOutline(false);
}
void Update() {
UpdateOutline(true);
onMouseDown();
}
private void onMouseDown()
{
Vector3 mousePos;
mousePos = Input.mousePosition;
mousePos = Camera.main.ScreenToWorldPoint(mousePos);
CapsuleCollider2D coll = GetComponent<CapsuleCollider2D>();
if (Input.GetMouseButtonDown(0))
{
if (coll.OverlapPoint(mousePos))
{
_currentColor = Color.clear;
if (!clicked)
{
_currentColor = OutlineColor;
clicked = true;
}
else
{
clicked = false;
}
}
}
if (Input.GetMouseButtonDown(1))
{
_currentColor = Color.clear;
}
}
void UpdateOutline(bool outline) {
MaterialPropertyBlock mpb = new MaterialPropertyBlock();
spriteRenderer.GetPropertyBlock(mpb);
mpb.SetFloat("_Outline", outline ? 1f : 0);
mpb.SetColor("_OutlineColor", _currentColor);
mpb.SetFloat("_OutlineSize", outlineSize);
spriteRenderer.SetPropertyBlock(mpb);
}
}
hatebin
hatebin
A bit strange for me that you go through a controller class just to again find all instances of the actual class ;)
First you could simplify your code a lot:
public class ObjectController : MonoBehaviour
{
// The list is already public so no need to have a getter method ...
public List<SpriteOutline> objects;
private void Start()
{
// FindObjectsOfType returns an SpriteOutline[]
// so the easiest way of converting it to a list is
objects = FindObjectsOfType<SpriteOutline>().ToList();
}
}
Then you would later simply do
FindObjectOfType<ObjectController>().objects
However this isn't even necessary!
Instead of this manager/Singleton pattern here I would rather use a static and let your object instances register themselves and let your class handle it completely itself:
public class SpriteOutline : MonoBehaviour
{
[SerializeField] private Color OutlineColor = Color.white;
[SerializeField] [Range(0, 16)] private int outlineSize = 1;
[SerializeField] private SpriteRenderer _spriteRenderer;
// Here you actually register and unregisters instances
private static readonly List<SpriteOutline> _instances = new List<SpriteOutline>();
// If even needed public read-only access
public ReadOnlyCollection<SpriteOutline> Instances => _instances.AsReadOnly();
// The current selection
private static readonly HashSet<SpriteOutline> _currentSelection = new HashSet<SpriteOutline>();
// Again if needed a public read-only access
public static HashSet<SpriteOutline> CurrentSelection => new HashSet<SpriteOutline>(_currentSelection);
// backing field for storing the actual value of selected
private bool _isSelected;
// Property which additionally updates the outline when changed
public bool IsSelected
{
get => _isSelected;
set
{
if(value == _isSelected) return;
_isSelected = value;
if(value)
{
_currentSelection.Add(this);
}
else
{
_currentSelection.Remove(this);
}
UpdateOutline(enabled, value);
}
}
private void Awake()
{
if(!_spriteRenderer) _spriteRenderer = GetComponent<SpriteRenderer>();
// Register yourself
_instances.Add(this);
}
private void OnEnable()
{
UpdateOutline (true, _isSelected);
}
private void OnDisable()
{
UpdateOutline (false, _isSelected);
}
private void OnDestroy ()
{
// Unregister yourself
_instances.Remove(this);
if(_currentSelection.Contains(this)) _currentSelection.Remove(this);
}
// This simplifies your query a lot since this is anyway only called
// If the mouse is hovering the collider of this object
private void OnMouseDown()
{
// Toggle selection of this
if (Input.GetMouseButtonDown(0))
{
IsSelected = !IsSelected;
}
}
private void Update()
{
// Deselect this
if (Input.GetMouseButtonDown(1))
{
IsSelected = false;
}
}
void UpdateOutline(bool outline, bool selected)
{
var mpb = new MaterialPropertyBlock();
_spriteRenderer.GetPropertyBlock(mpb);
mpb.SetFloat("_Outline", outline ? 1f : 0);
mpb.SetColor("_OutlineColor", selected ? OutlineColor : Color.clear);
mpb.SetFloat("_OutlineSize", outlineSize);
spriteRenderer.SetPropertyBlock(mpb);
}
}
If you rather want to select only exactly one object at a time then I would exchange the HashSet<SpriteOutline> _currentSelection by something like
// The current selection
private static SpriteOutline _currentSelection;
// Again if needed a public read-only access
public static SpriteOutline CurrentSelection => _currentSelection;
accordingly change
public bool IsSelected
{
get => _isSelected;
set
{
if(value == _isSelected) return;
// assuming that if you set this via property it should
// overwrite the current selection
// otherwise you would here do
//if(_currentSelection && _currentSelection != this) return;
if(_currentSelection && _currentSelection != this) _currentSelection.IsSelected = false;
_isSelected = value;
_currentSelection = value ? this : null;
UpdateOutline(enabled, value);
}
}
and finally you wanted to block the input on objects that are not the selection:
private void OnMouseDown()
{
// Toggle selection of this
if (Input.GetMouseButtonDown(0))
{
if(_currentSelection && _currentSelection != this) return;
IsSelected = !IsSelected;
}
}
Note: Typed on smartphone but I hope the idea gets clear

Item/Ability system using scriptableobjects in Unity

I'm new to Unity and I'm having some trouble creating scriptable objects for items to pick up and use in my project. I've created the initial set-up to make the items with variables that I can plug-and-play with.
What I'm trying to do is:
1 - plug in a monobehavior script for each new item's behavior when used.
2 - Allow the player to pick up the item on collision and use the item on key command. The player should only carry one item at a time.
Here is what I have so far.
the scriptable object (itemObject.cs)
[CreateAssetMenu(fileName = "New Item", menuName = "Item")]
public abstract class Item : ScriptableObject {
public new string name;
public string description;
public int damageAmount;
public int affectDelay;
public int affectDuration;
public int affectRange;
public float dropChance;
public float itemLife;
}
I have a script that will trigger if the player collides with the object (itemDisplay.cs)
Currently, this doesn't do anything but destroys the item.
public class itemDisplay : MonoBehaviour {
public Item item;
public static bool isActive = false;
void OnTriggerEnter(Collider other)
{
if (other.gameObject.tag == "Player")
{
var playerPickUp = other.gameObject;
var playerScript = playerPickUp.GetComponent<Player>();
var playerItem = playerScript.playerItem;
playerScript.pickUpItem();
bool isActive = true;
Object.Destroy(gameObject);
}
}
void Start () {
}
void Update(){
}
}
Currently, I have a pickup function and a use item function that's being handled by my player script(Player.cs). Currently, this just toggles a boolean saying that it did something when the item is collided with. My big question is how/where should I create and reference the item ability and how do I pass it to the player script here?
public void pickUpItem()
{
playerItem = true;
//itemInHand = itemInHand;
Debug.Log("You Picked Up An Item!");
// Debug.Log(playerItem);
if(playerItem == true){
Debug.Log("This is an item in your hands!");
}
}
public void useItem()
{
//use item and set
//playerItem to false
playerItem = false;
}
If your question is "how do I create an instance of an Item in my project", you do that by right clicking in your Project view, then selecting Create -> Item. Then drag the Item asset that you created from your Project view into the ItemDisplay scene object's item field in the Inspector.
Regarding how to pick up the item, you would pass the item to your Player script from your ItemDisplay script. You've got most of that already wired up.
public class ItemDisplay : MonoBehaviour
{
void OnTriggerEnter(Collider other)
{
if (other.gameObject.tag == "Player")
{
var playerPickUp = other.gameObject;
var playerScript = playerPickUp.GetComponent<Player>();
var playerItem = playerScript.playerItem;
// This is where you would pass the item to the Player script
playerScript.pickUpItem(this.item);
bool isActive = true;
Object.Destroy(gameObject);
}
}
}
Then in your Player script...
public class Player : MonoBehaviour
{
public void PickUpItem(Item item)
{
this.playerItem = true;
this.itemInHand = item;
Debug.Log(string.Format("You picked up a {0}.", item.name));
}
public void UseItem()
{
if (this.itemInHand != null)
{
Debug.Log(string.Format("You used a {0}.", this.itemInHand.name));
}
}
}
You should also make your Item class non-abstract, or leave it abstract and create the appropriate non-abstract derived classes (Weapon, Potion, etc.). I put an example of that below.
You can't attach a MonoBehaviour to a ScriptableObject if that's what you're trying to do. A ScriptableObject is basically just a data/method container that isn't tied to a scene. A MonoBehaviour has to live on an object within a scene. What you could do however, is add a "use" method to your Item class, and call that from a MonoBehaviour within the scene when the player uses an item.
public abstract class Item : ScriptableObject
{
// Properties that are common for all types of items
public int weight;
public Sprite sprite;
public virtual void UseItem(Player player)
{
throw new NotImplementedException();
}
}
[CreateAssetMenu(menuName = "Items/Create Potion")]
public class Potion : Item
{
public int healingPower;
public int manaPower;
public override void UseItem(Player player)
{
Debug.Log(string.Format("You drank a {0}.", this.name));
player.health += this.healingPower;
player.mana += this.manaPower;
}
}
[CreateAssetMenu(menuName = "Items/Create Summon Orb")]
public class SummonOrb : Item
{
public GameObject summonedCreaturePrefab;
public override void UseItem(Player player)
{
Debug.Log(string.Format("You summoned a {0}", this.summonedCreaturePrefab.name));
Instantiate(this.summonedCreaturePrefab);
}
}
Then change your UseItem method in Player:
public class Player : MonoBehaviour
{
public void UseItem()
{
if (this.itemInHand != null)
{
this.itemInHand.UseItem(this);
}
}
}

Access Serialized List Object for Custom Unity Editor Window

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.

Categories