I was coding a very simple program that lets you move around a circle, with also a rectangle in the stage. I wanted to make the circle get in front of the rectangle while you are dragging it, but when you released the mouse, the circle would be sent back.
I don't know how to set a public variable using the getChildIndex method. I don't really care about the rest of the code. I'm mainly interested in how can I make the getChildIndex method work with a public variable.
package code
{
import flash.display.MovieClip;
import flash.events.MouseEvent;
import flash.events.Event;
import flash.display.Sprite;
public class Main extends MovieClip
{
public var myCircleIndex:int = getChildIndex(myCircle);
public function Main()
{
myCircle.addEventListener(MouseEvent.MOUSE_DOWN, mouseClicking);
stage.addEventListener(MouseEvent.MOUSE_UP, mouseReleased);
}
public function mouseClicking(e:MouseEvent): void
{
myCircle.startDrag();
setChildIndex(myCircle, numChildren-1);
}
public function mouseReleased(e:MouseEvent): void
{
myCircle.stopDrag();
setChildIndex(myCircle, myCircleIndex);
}
}
}
I'm using an instance ("myCircle") that I created directly in the stage as a movie clip.
The problem is in the public var I set at the beginning, it doesn't let me get the child index of myCircle, but if I put the same line inside a function, it works.
I know I could directly put the index number of myCircle in the last line (and erasing the public var myCircleIndex), but I figured out that there would be a way of using the getChildIndex for a public var in a class.
How do you use getChildIndex in a public variable inside a class?
The reason it doesn't work, is because your timeline objects don't yet exist when the line public var myCircleIndex:int runs.
You shouldn't try and access non-primitive objects in your class level variable declarations for this very reason, as nothing else in the class is available yet when those vars are created.
Here is how you can refactor this (see the code comments):
public class Main extends MovieClip
{
public var myCircleIndex:int; //just create the reference here, don't assign it
public var myCircle:flash.display.DisplayObject; //this line is just for better compile time checking and code completion, completely optional
public function Main()
{
//wait for all the display stuff to be created before trying to access it. The constructor function can run before timeline stuff is created, so it's not safe to reference stage or timeline objects here.
if(!stage){
this.addEventListener(Event.ADDED_TO_STAGE, timelineCreated);
}else {
timelineCreated(null);
}
}
private function timelineCreated(e:Event):void {
//now that we're certain the timeline stuff has been created, we can reference timeline objects and stage:
//store the initial z-index of myCircle
myCircleIndex = getChildIndex(myCircle);
//the rest of your code that was in the construction -Main()- before
myCircle.addEventListener(MouseEvent.MOUSE_DOWN, mouseClicking);
stage.addEventListener(MouseEvent.MOUSE_UP, mouseReleased);
}
//no change to any of the following stuff
public function mouseClicking(e:MouseEvent): void
{
myCircle.startDrag();
setChildIndex(myCircle, numChildren-1);
}
public function mouseReleased(e:MouseEvent): void
{
myCircle.stopDrag();
setChildIndex(myCircle, myCircleIndex);
}
}
All you need to do to put the circle behind the square is on release do addChild(myRectangle) or addChildAt(myCircle, 0);
You are overcomplicating things by trying to track a variable in my opinion. Let flash sort it out behind the scenes.
If you want a little more finesse and want to just put the circle directly behind the square (if there were 100 layers and the square is at level 12, but you aren't sure which level the square is at) you could do
addChildAt(myCircle, getChildIndex(myRectangle)-1);
note
setChildIndex(myCircle, numChildren-1);
That's fine to do it that way. The more common way to do this is just
addChild(myCircle);
It does the exact same thing. Many people are confused by this thinking this would add a new myCircle but it just brings it to the front if it's already in the display list, and if it's not in the display list, it adds it to the display list at the top z-order (numChildren-1).
Related
What I want:
I want the player to be able to click on instantiated objects and get points, then have those points show in the score-keeping text.
What I’ve done:
I’m currently using the following “FindGameObjectsWithTag” code to retrieve the buttons that are components of the instantiated prefab objects:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
public class CPointScore : MonoBehaviour
{
public TextMeshProUGUI CPointsText;
private float ScoreNum;
private GameObject[] CButtonGmeObjsHolder;
private void CTagFinder()
{
CButtonGmeObjsHolder = GameObject.FindGameObjectsWithTag("Ctag");
foreach (GameObject CButtonGmeObj in CButtonGmeObjsHolder)
{
Debug.Log("GmeObj Found");
Button CButton = CButtonGmeObj.GetComponent<Button>();
CButton.onClick.AddListener(AddScore);
}
}
public void AddScore()
{
ScoreNum += 1;
Debug.Log("Point Added # " + ScoreNum);
}
void Start()
{
InvokeRepeating("CTagFinder", 1f, 15.1f);
}
void Update()
{
CPointsText.text = ScoreNum.ToString();
}
}
Because FindGameObjectsWithTag only calls once I have the InvokeRepeating code in start. I have game objects spawning throughout the duration of the game so it needs to be constantly checking for tags.
Issue:
So the code finds the tags, the buttons are able to be clicked, and the score-keeping text updates which is great. The problem is that if I click one tagged button it will register a point for itself and every tagged button currently in the scene that spawned after it. For example, lets say I have 4 spawned objects currently on scene, when the first object spawned is clicked it will add 4 points instead of 1. If the second object spawned is clicked it will add 3 points instead of 1. I would like to have only the tagged button that is clicked register a point.
Question:
What can I change in my code so that only the tagged button that is clicked registers a point?
Thank you
I think there are two things here:
You repeatedly add the listener so you will end up with multiple callbacks when the button is finally clicked.
The repeated FindGameObjectsWithTag is also quite inefficient
Your main issue is the repeated calling.
For each repeated call of CTagFinder you go through all existing buttons and do
CButton.onClick.AddListener(AddScore);
so these existing buttons end up with multiple listeners attached!
You either want to make sure it is only called once per button, e.g. keeping track of those you already did this for:
private readonly HashSet<Button> alreadyRegisteredButtons = new HashSet<Button>();
and then
if(!alreadyRegisteredbuttons.Contains(CButton))
{
CButton.onClick.AddListener(AddScore);
alreadyRegisteredButtons.Add(CButton);
}
or alternatively make sure you remove the callback before you add it like
CButton.onClick.RemoveListener(AddScore);
CButton.onClick.AddListener(AddScore);
In general I would not use FindGameObjectWithTag an poll objects repeatedly. Rather make your code event driven. This would already avoid the issue at all since there would be no repeated attaching of the listener anyway.
I would simply have a dedicated component YourComponent attached to the same GameObject as the buttons and have a global
public static event Action<YourComponent> OnCTSButtonSpawned;
and in this dedicated component do
private void Start()
{
OnCTSButtonSpawned?.Invoke(this);
}
and in your CPointScore listen to this event like
private void Awake()
{
YourComponent.OnCTSButtonSpawned += AttachListener;
}
private void AttachListener(YourComponent component)
{
if(compoenent.TryGetComponent<Button>(out var button))
{
button.onClick.AddListener(AddScore);
}
}
private void AddScore()
{
ScoreNum++;
CPointsText.text = ScoreNum.ToString();
}
Ok, so, a Slider in Unity has an "On Value Changed (Single)" property. As far as I've managed so far, you can set on it a callback function that can take a parameter (e.g. a string) which you hardcode in the Inspector, and/or a function that takes a float (the new value that the slider has been set to). However, I'd like to be able to pass both, and I haven't seen a way to do it. For example, I'd like to have a function saveValue(string name, float value), and several sliders feeding their values into it - each slider would have a different name written in the Inspector, and the updated value would be passed as value. I suspect Unity does not support that, but I haven't found any documentation explicitly describing this feature, so I'm not sure. Can this be done?
This should get you started:
Create a class that all of your sliders can access (for example by reference). And put the method you want them to call in there. The method should take the two parameters that you want to use (in my case I want it to take a GameObject and a float):
public class SliderManagerScript : MonoBehaviour
{
public void SliderValueChangeHandler(GameObject slider, float value)
{
Debug.Log("Slider name: " + slider.gameObject.transform.name);
Debug.Log("Slider value: " + value);
}
}
On each of the sliders that you want to call the above method, add a script that can access the above class and add an event listener for the Slider's onValueChanged property. Register a delegate for that listener which calls the method shown above and passes in the appropriate values. E.g
public class SliderScript : MonoBehaviour
{
public GameObject sliderManager;
void Start()
{
GetComponent<Slider>().onValueChanged.AddListener(delegate { sliderManager.GetComponent<SliderManagerScript>().SliderValueChangeHandler(this.gameObject, this.GetComponent<Slider>().value); });
}
}
And that should work fine. In fact, you should be able to simplify it a lot more depending on how you've architected your code.
Basically, I want to figure out how I can:
Save string (or any) variables in a custom editor window (inheriting
from EditorWindow) when they are changed in that window.
Display strings in a format like a TextArea while still allowing
saving changes as mentioned above.
Display strings from a string array by index, rather than individually defined strings (I've had trouble with this before)
If you know how to do the above in a custom inspector too
(inheriting from Editor, not EditorWindow), that'd be great too.
I've run into this issue a few times with different classes inheriting from Editor, and previously solved by using a PropertyField rather than a TextArea/TextField, but that gets rid of the TextArea-style formatting that I want.
Also, classes inheriting from EditorWindow don't seem to allow it in the same way (t = (script type)target; doesn't work, and PropertyField needs it)..?
I'm pretty new to custom inspectors and this stuff, so code examples would be super helpful if possible.
Thanks!
Before starting a general note because you mentioned it in your question:
Whenever possible I strongly recommend to avoid using target at all! In particular do not set any fields directly. this makes things like marking your scene direty and thus saving changes and also Undo/Redo functionality pretty complicated as you will have to implement it by yourself!
Rather always go through SerializedProperty combined with SerializedObject.Update and SerializedObject.ApplyModifiedProperties (examples will be below). This handles all this stuff like marking dirty and thus saving the scene changes and Undo/Redo automatically for you!
Then to the TextArea.
Lets say you have a class like
public class Example : MonoBehaviour
{
[SerializeField] private string _exampleString;
public string AnotherExampleString;
}
Basically there are three main options. I will do the Editor (custom Inspector) script first since there you are a bit more flexible.
The EditorWindow will be below.
Editor Attribute [TextArea]
Actually you wouldn't even need an Editor script at all! Simply tag the according field(s) as [TextArea] like this:
public class Example : MonoBehaviour
{
[SerializeField] [TextArea] private string _exampleString;
// You can also directly configure the min and max line count here as well
// By default it is 3 lines
[TextAre(3,7)] public string AnotherExampleString;
}
This already looks like this
EditorGUILayout.PropertyField
Then if you still need the Editor script the good thing about a EditorGUILayout.PropertyField is it automatically uses the correct drawer for the according type ... and it also applies all editor attributes! Isn't this great?
So simply having and Editor like
[CustomEditor(typeof(Example))]
public class ExampleEditor : Editor
{
private SerializedProperty _exampleString;
private SerializedProperty AnotherExampleString;
private void OnEnable()
{
// Link in the serialized properties to their according fields
_exampleString = serializedObject.FindProperty("_exampleString");
AnotherExampleString = serializedObject.FindProperty("AnotherExampleString");
}
public override void OnInspectorGUI()
{
DrawScriptField();
// load the real target values into the serialized properties
serializedObject.Update();
EditorGUILayout.PropertyField(_exampleString);
EditorGUILayout.PropertyField(AnotherExampleString);
// write back the changed properties into the real target
serializedObject.ApplyModifiedProperties();
}
// Little bonus from my side so you have the script field on top
private void DrawScriptField()
{
EditorGUI.BeginDisabledGroup(true);
EditorGUILayout.ObjectField("Script", MonoScript.FromMonoBehaviour((Example)target), typeof(Example), false);
EditorGUILayout.Space();
EditorGUI.EndDisabledGroup();
}
}
The result looks basically exactly the same:
EditorGUILayout.TextField
Using a EditorGUILayout.TextArea you can display any string as a multi-line text area. This also applies to an EditorWindow.
Lets say again we didn't tag our string fields
public class Example : MonoBehaviour
{
[SerializeField] private string _exampleString;
public string AnotherExampleString;
}
But we can make them appear just as before using this Editor script:
[CustomEditor(typeof(Example))]
public class ExampleEditor : Editor
{
private SerializedProperty _exampleString;
private SerializedProperty AnotherExampleString;
private Vector2 scroll1;
private Vector2 scroll2;
private void OnEnable()
{
// Link in the serialized properties to their according fields
_exampleString = serializedObject.FindProperty("_exampleString");
AnotherExampleString = serializedObject.FindProperty("AnotherExampleString");
}
public override void OnInspectorGUI()
{
DrawScriptField();
// load the real target values into the serialized properties
serializedObject.Update();
EditorGUILayout.PrefixLabel(_exampleString.displayName);
scroll1 = EditorGUILayout.BeginScrollView(scroll1,GUILayout.MaxHeight(3 * EditorGUIUtility.singleLineHeight));
_exampleString.stringValue = EditorGUILayout.TextArea(_exampleString.stringValue, EditorStyles.textArea);
EditorGUILayout.EndScrollView();
EditorGUILayout.PrefixLabel(AnotherExampleString.displayName);
scroll2 = EditorGUILayout.BeginScrollView(scroll2, GUILayout.MaxHeight(7 * EditorGUIUtility.singleLineHeight));
AnotherExampleString.stringValue = EditorGUILayout.TextArea(AnotherExampleString.stringValue);
EditorGUILayout.EndScrollView();
// write back the changed properties into the real target
serializedObject.ApplyModifiedProperties();
}
// Little bonus from my side so you have the script field on top
private void DrawScriptField()
{
EditorGUI.BeginDisabledGroup(true);
EditorGUILayout.ObjectField("Script", MonoScript.FromMonoBehaviour((Example)target), typeof(Example), false);
EditorGUILayout.Space();
EditorGUI.EndDisabledGroup();
}
}
Though you can see we already had to fake it a bit using the additional EditorGUILayout.BeginScrollView
This same thing you can also do in an EditorWindow. Most of the times it makes not much sense to go through SerializedProperty for EditorWindow
public class ExampleWindow : EditorWindow
{
private string exampleString;
private Vector2 scroll;
[MenuItem("Example/Show ExampleWindow")]
private static void Initialize()
{
var window = GetWindow<ExampleWindow>();
window.Show();
}
private void OnGUI()
{
EditorGUILayout.PrefixLabel("Example String");
scroll = EditorGUILayout.BeginScrollView(scroll,GUILayout.MaxHeight(3 * EditorGUIUtility.singleLineHeight));
exampleString = EditorGUILayout.TextArea(exampleString, EditorStyles.textArea);
EditorGUILayout.EndScrollView();
}
}
which results in
I have my own custom UnityEvent and am trying to add a listener.
I have used AddListener on numerous other UI objects, such as buttons, dropdowns, toggles, etc. so I understand the process. However, when I Invoke my UnityEvent, it simply doesn't fire.
I'm receiving no error messages, and after doing reading and research, everything looks correct. So, not sure what to do further.
This is an object that emits when it's rotated.
This is the basics of my code:
using UnityEngine.Events;
public class Rotator: MonoBehaviour
{
public UnityEvent OnRotate;
int angle = 0;
int newAngle = 0;
void Start()
{
OnRotate = new UnityEvent();
}
void Update()
{
newAngle = (int)transform.rotation.eulersAngles.z;
if (newAngle != angle)
{
print ("Actual Instance ID: " + GetInstanceID());
print ("Invoking!");
OnRotate.Invoke();
angle = newAngle;
}
}
}
and
public class Owner: MonoBehaviour
{
public Rotator rotator;
void Start()
{
print ("Rotator Instance ID: " + rotator.GetInstanceID());
rotator.OnRotate.AddListener(
() => UpdateRotation()
);
}
void UpdateRotation()
{
print ("Invoked!");
}
}
When the Rotator has it's angle changed, I get this in the console:
Actual Instance ID: 11234
Rotator Instance ID: 11234
Invoking!
The instance ID is to make sure I'm working with the same objects and not going in circles for nothing. They match, so I'm listening to the object that's firing.
However, the listener isn't firing. I've tried different combinations with delegates, etc. but it's all the same. No errors. It just doesn't invoke.
Obviously, I'm doing something wrong, but what is it?
Thanks for any help.
Somehow your answered your new edited version of the question with exactly the code you previously provided in the First Version of your Question!
As I tried to tell you ... if you anywhere in your code do OnRotate = new UnityEvent() of course you thereby erase any persistent callbacks and any runtime callbacks added before that moment!
In short
Simply leave it as
public UnityEvent OnRotate;
and you don't even have to think about it anymore.
For understanding why it also works if you put it in Awake please simply have a look at the Order of Execution for Event Functions
→ First Awake and OnEnabled is called for every GameObject/Component. Then all Start methods are called as soon as the GameObject/Component is active.
Within each of these blocks (Awake + OnEnable) and (Start) the order of execution between different component types is not guaranteed unless you explicitly configure it via the Script Execution Order Settings where you could define that Owner is simply run before Rotator .. then having both in Start would also work again.
Why does it also work if you do it on the public field?
→ Because this field is serialized. That means it is initialized automatically in the Inspector and then stored together with the Scene or prefab asset including any persistent callbacks.
And then Later Unity re-uses the serialized Version of the field so actually you can completely remove the new UnityEvent(); since it doesn't have any effect on a serialized field! It will always be initialized automatically anyway!
Ok, I found out what the issue was.
My question now is "why?".
I changed my code from:
public UnityEvent OnRotate;
void Start() {
OnRotate = new UnityEvent();
}
to
public UnityEvent OnRotate = new UnityEvent();
void Start() {
}
And now it works.
Although, now that I think about it, Awake() is the method where they all fire before initialization, whereas Start() is when the object is created. So the Start() of the Rotator is probably getting called after the Owner is adding a listener.
I'm learn as3, and building some exercises from the adobe online tutorials. I'm trying to do a very simple task and I think my code is exactly the same as the tutoriala nd it's not working! I just can't work it out...
I'm trying to change the textfield in a button from that buttons class.
The button is called GameButton, the textfield is called blabel and is classic text and dynamic text. Here is the code for the GameButton class. Instead of displaying "Click" as below it just changes the label to the number 1. The trace statement is working etc it is going there, but the text isn't passing through or something. Please help!!!
package {
import flash.display.MovieClip;
public class GameButton extends MovieClip {
public function GameButton() {
trace("Gamebutton has been created");
this.blabel.text = "Click";
stop();
}
}
}
The long and short of it is you can create the button in code, or else you can try listening for added to stage events coming from the parent object you're adding the children to (maybe the stage, maybe another DisplayObjectContainer). The problem with the listening method is I'm not sure how you would know which child just dispatched the event without making some messy code. I think the first option is generally easier and makes more sense, the only caveat is that you have to place the instances using x/y coordinates or apply scaleX, scaleY to stretch or shrink objects instead of doing it using the authoring tool. You can still use the drag and drop parts of flash to figure out coordinates and build individual movie clips etc.
Enough talk on to some code:
package
{
import flash.display.MovieClip;
public class GameButton extends MovieClip {
private var blabel:TextField; //This can be any display object class or a class that extends from a display object class (Sprite, MovieClip, MyCustomButton etc.)
public function GameButton() {
blabel = new TextField(); //The type after new, should be the same, or a sub-class (extension) of the type used in the variable declaration above
addChild(blabel);
//blabel.x=10; //optional over ten pixels from left
//blabel.y=10; //optional down ten pixels from top
//blabel.scaleX=.5; //optional half the width
//blabel.scaleY=2; //optional 2 times taller
trace("Gamebutton has been created");
blabel.text = "Click";
stop();
}
}
}