I'm building a mobile game with many scenes (AKA Levels) in Unity, I also intend on adding more levels through updates later in the games life cycle. To cope with this I am attempting the make an automated script that see's how many scenes there are in the Unity build, and create corresponding UI buttons based on a prefab.
For clarity sake I'll note that when I say "scenes that are in ther Unity Build", I mean scenes that are in the build menu:
And already have an index number.
It's not a lot but here is my current script which I have a levelManager in the scene:
private Scene[] levels;
private void Start()
{
/// This is the line I'm requesting help with
levels = // Some way of getting all the scene's in a project put in an array
foreach (var item in levels)
{
// I will handle the button creation and modification later that will go here
}
}
As said you don't create that variable yourself but rather can simply read out SceneManager.sceneCountInBildSettings and then use SceneManager.GetSceneByBuildIndex in order to iterate and get all these scenes like
// Adjust this in the Inspector
// Set this to the index of level 1
[SerializeField] private int startIndex = 1;
var sceneCount = SceneManager.sceneCountInBildSettings;
levels = new Scene[sceneCount - startIndex];
for(var i = startIndex; i < sceneCount; i++)
{
level[i] = SceneManager.GetSceneByBuildIndex(i);
}
Alternatively you could also do it already before runtime via an editor script using EditorBuildSettings.scenes e.g.
#if UNITY_EDITOR
using UnityEditor;
#endif
...
// Make this field serialized so it gets stored together with the scene
[SerializeField] private Scene[] levels;
#if UNITY_EDITOR
[ContextMenu(nameof(StoreScenes))]
private void StoreScenes()
{
levels = EditorBuildSettings.scenes;
EditorUtility.SetDirty(this);
EditorSceneManager.MarkSceneDirty(SceneManager.GetActiveScene());
}
#endif
you would run this method before the build by going to the Inspector of your component, open the context menu and hit StoreScenes and it will fill your array. The #if UNITY_EDITOR has to wrap anything using the UnityEditor namespace as it will be stripped of in a build and you would get build exceptions otherwise.
Related
In my game I have a big catalog of gear: Armors, weapons and shields. The combinations between these can be really immense.
Besides that, the player has the option of switching in-game to a different set of armor-weapon combination. In the end to solve this, I have used the following object structure.
Whenever I switch the weapons, I activate/deactivate the necessary GameObjects. The animations are set in this way:
Now, the problem is creating the animation. I first considered pre-rendering programatically all the combinations, but my catalog is so huge, that it would create 100s, if not 1000s of animations. So I opted for a different solution. Create in playtime the animation, once I knew what gear would the player select. For that, I created a script to take care of that. The problem is that I have been using APIs from UnityEditor, and now I have realized the build will not work. Specifically because of 2 different classes: EditorCurveBinding and ObjectReferenceKeyframe.
This is a couple snippets of how I was using this classes when creating the animations:
static EditorCurveBinding GetEditorCurveBinding(string path = "")
{
EditorCurveBinding spriteBinding = new EditorCurveBinding();
spriteBinding.type = typeof(SpriteRenderer);
spriteBinding.path = path;
spriteBinding.propertyName = "m_Sprite";
return spriteBinding;
}
static ObjectReferenceKeyframe GetKeyframe(float time, Sprite sprite)
{
ObjectReferenceKeyframe keyframe = new ObjectReferenceKeyframe();
keyframe.time = time / FRAMERATE;
keyframe.value = sprite;
return keyframe;
}
Now, the problem with the Curve, I think I managed to solve, replacing it with this code, replacing EditorCurveBinding with AnimationCurve:
AnimationClip clip = ...
AnimationCurve curve = new AnimationCurve();
clip.SetCurve(path, typeof(SpriteRenderer), "m_Sprite", curve);
But I have no idea how to set the sprites for each animation. I thought that using curve.AddKeycould be helpful, but I have seen no way to add a sprite there.
How could I rewrite that code to avoid using UnityEditor?
Full code
Personally, I would completely avoid built-in Animator and Animations, since this tool is for the very narrow purpose of animating a single object in a predefined way (eg: for a cutscene).
Offtopic - performance
Besides that, the player has the option of switching in-game to a different set of armor-weapon combination. In the end to solve this, I have used the following object structure.
As you probably know this is very inefficient memory-wise and will decrease performance (disabled objects still have marginal CPU overhead). And will incread load time and instantiation time of new objects.
Can I use Animator or Animation?
Because Animator has no API to access it's internals and animation type and overall you cannot do pretty mutch nothing with it except from telling it to Play or Stop.
Since I understand that your animation is "Sprite based" and not "Transform based" any sane idea is just inefficient!
Best solution that I vow against is as follows:
Create "trigger points" that you would "animate"
Based on trigger point - change sprite
public class AnimationController : MonoBehaviour
{
public int AnimationIndex;
public Sprite[] AnimationSprites;
public SpriteRenderer SpriteRenderer;
private void Update()
{
SpriteRenderer.sprite = AnimationSprites[AnimationIndex];
}
}
Better solution?
Since we already need to have custom structure that manages our sprites we might want to optimize the whole thing, since we are not using almost any of the features of Animator we could write custom controller that would replace Animator in this case. This should improve performance significantly since Animator is very heavy!
// MonoBehaviour is optional here
public class SpriteRendererAnimationHandler
{
// More fields that would control the animation timing
public Sprite[] AnimationSprites;
public SpriteRenderer SpriteRenderer;
public void OnAnimationUpdate(int index)
{
var resolvedIndex = ResolveIndex(index);
SpriteRenderer.sprite = AnimationSprites[resolvedIndex];
}
private int ResolveIndex(int index)
{
// Resolve animation index to sprite array index
}
}
// One controller per character - or global per game that synchronize all animations to locked FPS 12.
public class AnimationController : MonoBehaviour
{
private List<SpriteRendererAnimationHandler> Handlers = new List<SpriteRendererAnimationHandler>();
public void FixedUpdate()
{
foreach (var handler in Handlers)
{
// Calculate animation index
int calculatedAnimationIndex = ...;
handler.OnAnimationUpdate(calculatedAnimationIndex);
}
}
}
Add a public field named animation index to Tomasz last example, then create the animations in animator as youd normal do for animation pieces, then animate that animation index field. simple if(currentAnimationyion != lastAnimationIndex) to check if need to send to handlers and ya rocking
In Unity3D Version 2017 you could add multiple EventTriggers at once by doing this:
for (int i = 0; i < 20; i++)
{
EventTrigger.Entry pDown = new EventTrigger.Entry();
pDown.eventID = EventTriggerType.PointerDown;
pDown.callback.AddListener((eventdata) => { InventorySlotCopy(); });
slots[i].GetComponent<EventTrigger>().triggers.Add(pDown);
EventTrigger.Entry pUp = new EventTrigger.Entry();
pUp.eventID = EventTriggerType.PointerUp;
pUp.callback.AddListener((eventdata) => { InventorySlotInsert(); });
slots[i].GetComponent<EventTrigger>().triggers.Add(pUp);
}
where slots is just an array of GameObjects, each with an Image and an EventTrigger attachted to it.
However, using the same code as above in Unity3D Version 2019 results in adding those EventTriggers but not in assigning the functions to the Listener.
How is that been done in Unity2019?
First of all:
You will not see the added listeners in the Inspector!
In the Inspector you only see the permanent listeners, not the ones added on runtime! These are basically UnityEvent<BaseEventData>'s so see Manual: UnityEvents
When a UnityEvent is added to a MonoBehaviour it appears in the Inspector and persistent callbacks can be added.
The only way to add permanent listeners is either via the Inspector or a custom EditorScript! But this is not what you want to do here anyway.
I would slightly modify your script to make it more secure. I simply log some messages for the methods when they get called to show that they get called.
public class Example : MonoBehaviour
{
// Rather make these directly of type EventTrigger
// So you don't have to use GetComponent later and can be sure that these array only receives
// objects that actually have that Component attached!
public EventTrigger[] slots;
private void Start()
{
// Instead of a hardcoded index either iterate using slots.Length
// or simply directly foreach
foreach (var slot in slots)
{
var pDown = new EventTrigger.Entry
{
eventID = EventTriggerType.PointerDown
};
pDown.callback.AddListener(eventData => { InventorySlotCopy(); });
slot.triggers.Add(pDown);
var pUp = new EventTrigger.Entry
{
eventID = EventTriggerType.PointerUp
};
pUp.callback.AddListener(eventData => { InventorySlotInsert(); });
slot.triggers.Add(pUp);
}
}
private void InventorySlotCopy()
{
Debug.Log(nameof(InventorySlotCopy));
}
private void InventorySlotInsert()
{
Debug.Log(nameof(InventorySlotInsert));
}
}
The rest depends on your setup. We don't know what objects your slots are but there are basically two(three) options:
Option 1 - UI
If these slot objects are UI elements in a Canvas such as Image or Text.
This seems to be the case for your specifically.
Make sure that there is an EventSystem present in the scene.
Usually one gets created when creating the first Canvas.
If not create it now via Hierachy View → right click → UI → EventSystem
Make sure you have the option RayCast Target enabled on your slot's UI Components.
Result:
Options 2 - 3D Objects (Option 3 - 3D Objects with 2D Colliders)
Just adding this for other users.
If these objects are not UI elements but rather 3D objects make sure
Again you need the EventSystem as before
The slots have Collider Components attached to the same GameObject the EvenTrigger Component is attached to or any of its children (it may be nested).
Your Camera needs the Component PhysicsRaycaster attached. Make sure here that the eventMask includes the layer(s) your slot objects are placed on. (For Option 3 using 2D Colliders instead use the Physics2DRaycaster)
Result:
As you can see the methods will not be visible in the Inspector (due to the reason mentioned before) but the added listener methods still get called.
I have a list of Objects. They are Unity scenes.
public Object[] scenes;
I would like to fill this array with unity scenes, as a convenient way of storing the sequential order they should load within Unity's inspector.
I attempted to use the SceneManager to load a level using a reference to this array
public int currentSceneNumber = 0;
public void LoadNext()
{
if (scenes.Length >= currentSceneNumber + 1)
{
currentSceneNumber += 1;
SceneManager.LoadScene(scenes[currentSceneNumber].ToString());
}
else
{
Debug.Log("Level.cs: Unable to load next level.");
return;
}
}
But turning the scene into a string results in 'scenename(UnityEngine.SceneAsset)' The (login (UnityEngine.SceneAsset) is the problem part. It will result in the following error
Scene 'prototype02_Scene (UnityEngine.SceneAsset)' couldn't be loaded because it has not been added to the build settings or the AssetBundle has not been loaded.
To add a scene to the build settings use the menu File->Build Settings...
How do I get the scene name as a string from the scene object in my array?
You can't do this outside the Editor.
When you use public UnityEngine.Object[] scenes; to hold the Scenes, Unity will use SceneAsset as the placeholder to hold those scenes.
The problem with this is that SceneAsset is from the UnityEditor namespace which means that your code will only work in the Editor with the the AssetDatabase. You can't build it to run on any standalone platform. You will get this error if you attempt to build your project.
This is how to get all the scenes and store them in an array(Should work in the Editor and build):
public Scene[] scenes;
...
void initScenes()
{
int sceneCount = SceneManager.sceneCountInBuildSettings;
scenes = new Scene[sceneCount];
for (int i = 0; i < sceneCount; i++)
{
scenes[i] = SceneManager.GetSceneByBuildIndex(sceneCount);
}
}
Your loading code:
SceneManager.LoadScene(scenes[currentSceneNumber].name);
If you are sure that this is an Editor plugin and not something you need to build or run outside the Editor then here is your answer:
First of all, you are using the Object name which has nothing to do with the scene name since Object.ToString() is an override function that retrieves the
name of the Object.
Just cast the Object array to SceneAsset then access the name of the scene from that instead of the name of the Object like you are currently doing.
SceneManager.LoadScene(((SceneAsset)scenes[currentSceneNumber]).name);
or
SceneAsset scene = scenes[currentSceneNumber] as SceneAsset;
SceneManager.LoadScene(scene.name);
So I'm basically brand new to unity and C# and have been following along with tutorials online (N3K 2D Platformer, YouTube), I'm trying to create a basic UI to display score etc and I seem to have come across a null pointer exception which I can't seem to figure out as I have referenced the two objects that are causing this error, namely my scoreText object and my hitPointText object.
As I've said I did reference those very objects by dragging them from my hierarchy and into the fields I had created in my level manager script in the inspector and further to that I am simply following along with a tutorial and have done exactly as the video has instructed, but yet on the video it seems to work fine.
The offending lines of code are:
scoreText.text = score.ToString();
hitPointText.text = hitPoints.ToString();
This tutorial is now over 1 year old, is it possible that a unity update has changed that way things NEED to be referenced?
I'll post my level manager code below in the hopes that someone may be able to point out the error that I seem to be missing.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class LevelManager : MonoBehaviour
{
public static LevelManager Instance { set; get; }
public Transform spawnPosition;
public Transform playerTransform;
private int hitPoints = 3;
private int score = 0;
public Text scoreText;
public Text hitPointText;
private void Awake()
{
Instance = this;
scoreText.text = score.ToString();
hitPointText.text = hitPoints.ToString();
}
// Use this for initialization
void Start ()
{
}
// Update is called once per frame
private void Update ()
{
if(playerTransform.position.y < (-10))
{
playerTransform.position = spawnPosition.position;
hitPoints--;
if(hitPoints <= 0)
{
Debug.Log("Your Dead!");
}
}
}
public void Win()
{
Debug.Log("Victory");
}
}
Snippets of screens below:
Scene view of unity engine
Game view of unity engine, with game running
So here is a snippet of code from my player class which uses Instance on the LevelManager script in order that it can have access to the win() method as can be seen in the last case of the switch "WinPost", not sure if that is what you are referring to when your mentioning singleton, other than that never is the term singleton used in any of the scripts I have.
switch (hit.gameObject.tag)
{
case "Coin":
Destroy(hit.gameObject);
break;
case "JumpPad":
verticalVelocity = jumpForce * 2;
break;
case "Teleporter_1":
controller.enabled = false;
transform.position = hit.transform.GetChild(0).position;
controller.enabled = true;
Debug.Log("This works!");
break;
case "Teleporter_2":
controller.enabled = false;
transform.position = hit.transform.GetChild(0).position;
controller.enabled = true;
Debug.Log("This works!");
break;
case "WinPost":
LevelManager.Instance.Win();
break;
default:
break;
}
My guess would be that the components aren't initialized when you call Awake. Awake gets called as a constructor-kind-of method as soon as the object is created. When it is called, you can't be sure if the other components got initialized already.
I would suggest you copy the assignments you make in Awake into Start and come back to see if it works. Start gets called after the GameObjects have their components initialized.
private void Awake()
{
Instance = this;
}
// Use this for initialization
void Start ()
{
scoreText.text = score.ToString();
hitPointText.text = hitPoints.ToString();
}
Thanks to everyone for trying to help and all the great suggestions.
Ultimately I ended up breaking my game in the process of trying to recreate the same UI in a new blank scene, I did manage to recreate the same error before breaking my game which at the time then left me none the wiser. However due to the fact I broke my game I had to step back at least two tutorials and recreate the level manager object and the empty child spawnPosition object, (the level manager script was ok, it was just the level manager object and its child that I broke), anyway in having to recreate both of those objects again everything now seems to work as intended and so this leads me to the conclusion that the problem was not the code but the objects themselves???
Thanks again to everyone that tried to help, another day another learning experience.
D.
First of all i want to say Sorry for my bad english and bad grammar
i have a problem and that is when i press play in the editor my array i made in my custom editor disapares(also does that when i update the script)!
First i got a script called “ColorChangerSingle” which is the script i declare varibles
using UnityEngine;
public class ColorChangerSingle
{
public GameObject gameObjectToChange;
public Color color;
}
then i have a script called “ColorChanger” which is the script i make a custom inspector for and all it got is a static list of “ColorChangerSingle”
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ColorChanger : MonoBehaviour {
public static List<ColorChangerSingle> single = new List();
}
and i have the custom inspector script called “CustomChangeColorInspector” which is the custom inspector script.
using UnityEditor;
using UnityEngine;
[CustomEditor(typeof(ColorChanger))]
public class CustomColorChangerInspector : Editor
{
public override void OnInspectorGUI()
{
for (int i = 0; i < ColorChanger.single.Count; i++)
{
EditorGUILayout.BeginHorizontal();
ColorChanger.single[i].gameObjectToChange = (GameObject)EditorGUILayout.ObjectField(ColorChanger.single[i].gameObjectToChange, typeof(GameObject));
ColorChanger.single[i].color = EditorGUILayout.ColorField(ColorChanger.single[i].color);
EditorGUILayout.EndHorizontal();
if (ColorChanger.single[i].gameObjectToChange != null)
if (ColorChanger.single[i].gameObjectToChange.GetComponent() != null)
ColorChanger.single[i].gameObjectToChange.GetComponent().material.color = ColorChanger.single[i].color;
}
EditorGUILayout.BeginHorizontal("box");
if (GUILayout.Button("Add To Array"))
{
ColorChanger.single.Add(new ColorChangerSingle());
}
if (GUILayout.Button("Remove Object In Array"))
{
ColorChanger.single.RemoveAt(ColorChanger.single.Count - 1);
}
EditorGUILayout.EndHorizontal();
}
}
when i add arrays in “not play mode” everything works(setting objects / changing the color of them) but when i press play the array gets “reset”, i think it has to do with the “ColorChanger” script where i set the list equal to a new list of ColorChangerSingle :/
any help is greatly appreciated!
Pictures:
https://gyazo.com/167ab826b6d578ec5a66d9d2586479e8
https://gyazo.com/847a063f9885478200c5a504be1dae2a
thanks for your time and have a great day! //Jrp0h
btw i hope the catagory is good and i know i can clean up the code alot but i made this really quick becuse im working on a secret project and did not want to use that code :)
I don't think your problem comes from the public static List<ColorChangerSingle> single = new List(); line.
What I'd recommend is adding [SerializeField] attributes to your single field and [System.Serializable] to your ColorChangerSingle class. Also are you sure your scene is saved before entering Play mode (this is a common mistake I used to do earlier on) ? If not you can add something like this at the end of the OnInspectorGUI() method :
if(GUI.changed && !Application.isPlaying)
{
EditorUtility.SetDirty(m_Target);
EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene());
}
EDIT : Also you have to give your custom inspector script a reference to the instance of the script you want to edit (think of many of your GameObjects holding a ColorChanger script), when you call ColorChanger.single[i].gameObjectToChange = [...]; your CustomColorChangerInspector inspector script doesn't know which of your GameObject you refer too.
This is why you have to reference it. The way I usually do it for quick custom inspetocrs (there is more than one way to do it, using serialization for example) is :
[CustomEditor(typeof(ColorChanger))]
public class CustomColorChangerInspector : Editor
{
// I like to declare it once for all but you can also call "(ColorChanger)target" each time to refer to the target
private ColorChanger m_Target;
public override void OnInspectorGUI()
{
m_Target = target as ColorChanger;
for (int i = 0; i < ColorChanger.single.Count; i++)
{
EditorGUILayout.BeginHorizontal();
m_Target.single[i].gameObjectToChange = (GameObject)EditorGUILayout.ObjectField(m_Target.single[i].gameObjectToChange, typeof(GameObject));
[...]
}
}
}