How to automatically delete Event Listener from an object in AS3 - actionscript-3

I am working on small flash game. The game contains 20 levels and main menu. The transition between levels is made by deleting every object on the frame and also all event listeners. Then the code adds objects from the next level...
Catching and the removing event listeners is done by this code:
override public function addEventListener(type:String, listener:Function, useCapture:Boolean=false, priority:int=0, useWeakReference:Boolean=false):void
{
super.addEventListener(type, listener, useCapture, priority, useWeakReference);
arrListeners.push({type:type, listener:listener});
}
private function clearEvents():void
{
for(var i:Number = 1; i<arrListeners.length; i++){
if(this.hasEventListener(arrListeners[i].type))
{
this.removeEventListener(arrListeners[i].type, arrListeners[i].listener);
}
}
arrListeners = []
}
This code overrides internal addEventListeners and makes every Listener to be added in an array. Second function checks if the EventListeners is still there(not prevoiusly removed) and the just remove every Listener from the array.
This code works fine for EventListeners that are assigned to the stage. However, when an EventListener is assigned directly to an Object then it's not added to the array, so it doesn't get removed automatically later.
I know that when you remove the object, also you remove the Event Listeners assigned to it. But when I add that object again the Listeners run twice. You can freely move through levels, so you can go back and forth. And when you go back I recieve problems. System is overused and is woring slower, because the amount of Event Listeners that are running is doubled.
So, can you modify this code or give me an advice how can I catch EventListeners that are assigned to Object and eventually remove them.
Code:
package
{
Public Class Main extends MovieClip
{
Public function Main()
{
Intro();
}
Private function Intro():void
{
//Constructor contains a lot of addChild and a EventListeners. So I will upload what I think i important for this problem.
Play_btn.addEventListener(MouseEvent.CLICK, clicked);
function clicked (e:MouseEvent):void
{
clearEvents();
clearChild(); // function that removes all children on stage
FirstLevel();
}
}
Private function FirstLevel():void
{
//Also adding children and EventListeners, that affect the gameplay
Next_level_btn.addEventListener(MouseEvent.CLICK, clicked1);
function clicked1 (e:MouseEvent):void
{
clearEvents();
clearChild();
SecondLevel();
}
Main_Menu_btn.addEventListener(MouseEvent.CLICK, clicked1);
function clicked1 (e:MouseEvent):void
{
clearEvents();
clearChild();
Intro();
}
}
And so on for the next 20 levels.
Thanks in advice.

Arrays indices start from 0, clearEvents should be :
private function clearEvents():void
{
for(var i:int= 0; i<arrListeners.length; i++){
if(this.hasEventListener(arrListeners[i].type))
{
this.removeEventListener(arrListeners[i].type, arrListeners[i].listener);
}
}
arrListeners = []
}
Not sure if that will fix your problem though. If you have event listeners that are created when you add new objects you should remove those listeners when the object is destroyed/removed.

Removing an object (removeChild(object)) does NOT automatically remove it's event listeners. You would need to do that yourself. Something like this could work:
in your class constructor:
super.addEventListener(Event.ADDED_TO_STAGE,addedToStage,false,0,true); //only if you want the listeners added back again the next time this object is added to the stage eg. addChild(this)
super.addEventListener(Event.REMOVED_FROM_STAGE,removedFromStage,false,0,true);
The handlers:
//this runs whenever the object is added to the display list
//if you don't want the listeners re-added, remove this function.
private function addedToStage(e:Event):void {
for(var i:int=0; i<arrListeners.length; i++){
super.addEventListener(arrListeners[i].type, arrListeners[i].listener, arrListeners[i].useCapture, arrListeners[i].priority, arrListeners[i].useWeakReference);
}
}
//this runs whenever the object is removed from the display list
private function removedFromStage(e:Event):void {
for(var i:int=0; i<arrListeners.length; i++){
super.removedEventListener(arrListeners[i].type, arrListeners[i].listener, arrListeners[i].useCapture);
}
//OR if you want the listeners gone forever, use your clearEvents() method instead of the for loop above
}
This would make your listeners stop listening when the item is removed from the display list, and re-add them when added. You'd have to modify your array to include the other listener information like capture phase and weakReference. If you don't want them added again, just call your clearEvents() in the removedFromStage handler and take out the addedToStage listener/handler altogether.
This is assuming that the code you posted (and my additions) is the base-class of all the object you want it applied to.

Related

How to get the width of a MovieClip for a different frame instantly?

Is there a way to get the width of a MovieClip (that does have a name) on a different frame? I have tried to using .width and .getBounds(null).width, however, both of them will give me only the width of the current frame. I have tried to do gotoAndStop(frameiwant), but the information doesn't seem to be correct until at least the next frame
I would like to get the width of the frame instantly so I don't have to wait until the next frame for the width.
The only way I could think of doing this was to have an initial phase in your project which will:
Run through all of the frames in your timeline. Create an object which will hold information about the children in that frame. It can be called Frame.
Iterate over all the children that are added to the stage in that frame and add a definition object that describes that child. The description can be as basic or vast as you need. We can call this class an ObjectDefintion.
The downside of this process is that you need to wait for the FRAME_CONSTRUCTED event like #Larusso pointed out in his answer. This means that the frame actually has to finish rendering before you are able to get information about its children, which of course means you have to go through and render every single frame in your timeline during this phase. All you can really do to mitigate this problem is set the frameRate to something high and then set it back when you're done assessing all the frames.
I have set this up and it works well - I'll paste each class and try explain what they do.
So for your document class (or whichever MovieClip holds the frames you want to look at), I have this:
public class Main extends MovieClip
{
private var _userFrameRate:int;
private var _frames:Vector.<Frame> = new <Frame>[];
public function Main()
{
_userFrameRate = stage.frameRate;
stage.frameRate = 120;
addEventListener(Event.FRAME_CONSTRUCTED, _assess);
}
public function getFrame(index:int):Frame
{
return _frames[index - 1];
}
private function _assess(e:Event):void
{
var frame:Frame = new Frame(this);
_frames.push(frame);
if(currentFrame === totalFrames)
{
removeEventListener(Event.FRAME_CONSTRUCTED, _assess);
gotoAndStop(1);
stage.frameRate = _userFrameRate;
ready();
}
else play();
}
public function ready():void
{
// Start here.
// There is a MovieClip on frame 10 with the instance name 'test'.
// We can get the width of it like this.
trace( getFrame(10).define("test").property("width") );
}
}
This basically initializes the phase in which we will run over each frame in the MovieClip and assess its children. The ready() method is used as the entry point for your code post-assessment.
Next we have the Frame class, which serves to hold information about children related to a frame:
public class Frame
{
private var _main:Main;
private var _content:Object = {};
public function Frame(main:Main)
{
_main = main;
update();
}
public function update():void
{
_content = {};
for(var i:int = 0; i < _main.numChildren; i++)
{
var target:DisplayObject = _main.getChildAt(i);
// This will be explained below.
var definition:ObjectDefinition = new ObjectDefinition(target, "x", "y", "width", "height");
_content[target.name] = definition;
}
}
public function define(name:String):ObjectDefinition
{
return _content[name];
}
}
It's pretty straightforward - you give it a reference to Main so that it can check children that are existent within it each frame.
The ObjectDefinition class is also pretty straightforward, acting purely as a repository for data that you want to keep track of on each child of the frame:
public class ObjectDefinition
{
private var _definition:Object = {};
public function ObjectDefinition(target:DisplayObject, ...properties)
{
for each(var i:String in properties)
{
_definition[i] = target[i];
}
}
public function property(property:String):*
{
return _definition[property];
}
}
You'll notice that the constructor accepts the target DisplayObject that will be defined, as well as any amount of properties you want to keep track of as strings (see above within Frame for implementation).
Once complete, you can chain the methods Main.getFrame(), Frame.define() and ObjectDefinition.property() to get properties of children that will exist throughout the timeline. For example, if you have a MovieClip with the instance name square on frame 15 and you want to get its width and height, you can do this within .ready() like so:
var square:ObjectDefinition = getFrame(15).define("square");
trace(square.property("width"), square.property("height"));
Of course this process is not ideal - but unfortunately it is the only way I can see that what you want to achieve is possible.
You have to listen to a specific event before you can ask for the information.
clip.addEventListener(Event.FRAME_CONSTRUCTED, frameReadyHandler);
clip.gotoAndStop(frame);
function frameReadyHandler(event:Event):void
{
clip.removeEventListener(Event.FRAME_CONSTRUCTED, frameReadyHandler);
var width = clip.width;
}
The Frame constructed event is the first of several events that gets dispatched. It gets dispatches right before the frame script gets executed. You could also wait for the on enter frame event.
You could add an event listener for 1 millisecond and test if the previousWidth you had stored is different. If it is, there you go. If not, its probably listening to the same frame.
A 1 millisecond timer is not such a big deal, stop it if you don't need it, resume it if you do, else, keep it running constantly. When it changes, dispatch an event or whatever needs to happen.
If you know the maximum size of the MovieClip, you may try this:
// Create movie clip
var movie :MovieClip = new MovieClipWith3Frames();
// Move to second frame
movie.gotoAndStop(2);
// Create bitmap witch magenta background
var bd :BitmapData = new BitmapData(200, 200, false, 0xFF00FF);
// Draw second frame
bd.draw(movie);
// Found the bounds of shape
var movieBounds:Rectangle = bd.getColorBoundsRect(0xFFFFFF, 0xFF00FF, false);
trace(movieBounds); // (x=42, y=15, w=32, h=33)

ActionScripting Issue: adding/removing children

I have three movie clips all linked to the stage and I want them to behave like a button/ But I am not using a button because I have not found a way to have each part (up, over, down, hit) be animated and not just change when the mouse is in use with it. So far I have been able to have all three appear on my stage and show when I have the mouse over and as well when I click, but I think I'm doing something wrong with removeChild. Each MC should appear one at a time and now all three show up when I hover over and seem to "flash". Here's my code:
var mainMoon:swayingMoon = new swayingMoon();
mainMoon.x = 50;
mainMoon.y = 10;
addChild(mainMoon);
var hoverMoon:glowMoon = new glowMoon();
hoverMoon.x = 50;
hoverMoon.y = 10;
var movieMoon:clickedMoon = new clickedMoon();
movieMoon.x = 50;
movieMoon.y = 10;
mainMoon.addEventListener(MouseEvent.ROLL_OVER, showHoverMoon);
mainMoon.addEventListener(MouseEvent.ROLL_OUT, hideHoverMoon);
hoverMoon.addEventListener(MouseEvent.CLICK, startMovieMoon)
function showHoverMoon(event:MouseEvent):void
{
addChild(hoverMoon);
}
function hideHoverMoon(event:MouseEvent):void
{
removeChild(hoverMoon)
}
function startMovieMoon(event:MouseEvent):void
{
addChild(movieMoon);
}
I don't recommend doing it this way as it can make things needlessly complex. For a single button now, you have 3 times as many movie clips/sprites, add/remove child event handlers, and other variables to check/debug for. Multiply this by however many buttons you have.
Instead, I'd recommend using or extending the SimpleButton{} class or writing your own class to encapsulate the behaviour.
SimpleButton class: http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/display/SimpleButton.html
I think you should encapsulate your three moon states into a separate MovieClip that will control all the moon phase changing by itself. If it's already so, fine. The general principle with such a MovieClip-Button type objects is that the listener is assigned to the parent instance, not the parts of that button.
public class Moon extends Sprite {
private var upState:swayingMoon=new swayingMoon();
private var overState:glowMoon=new glowMoon();
private var downState:clickedMoon=new clickedMoon();
private var areWeClicked:Boolean;
private var areWeOver:Boolean;
public function Moon() {
areWeClicked=false;
areWeOver=false;
addChild(upState);
addEventListener(MouseEvent.ROLL_OVER, showHoverMoon);
addEventListener(MouseEvent.ROLL_OUT, hideHoverMoon);
addEventListener(MouseEvent.CLICK, showClickedMoon);
addEventListener(Event.COMPLETE, hideClickedMoon);
}
private function showHoverMoon(e:MouseEvent):void {
areWeOver=true;
if (areWeClicked) return;
removeChild(upState);
addChild(overState);
}
private function hideHoverMoon(e:MouseEvent):void {
areWeOver=false;
if (areWeClicked) return;
removeChild(overState);
addChild(upState);
}
private function showClickedMoon(e:MouseEvent):void {
if (areWeClicked) {
downState.gotoAndPlay(1);
return;
}
if (overState.parent) removeChild(overState); else removeChild(upState);
addChild(downState);
downState.gotoAndPlay(1); // your clicked moon seems to be a playing MC, so starting it over
areWeClicked=true;
}
private function hideClickedMoon(e:Event):void {
if (e.target!=downState) return; // not our event
if (!areWeClicked) return;
areWeClicked=false;
removeChild(downState);
if (areWeOver) addChild(overState); else addChild(upState);
}
}
Now, your parent class is controlling what happens and when. I was under assumption that your clickedMoon MC will only play once fully, then dispatch an event Event.COMPLETE to itself, so that its parent will get notified and will act.
Originally, your event listener structure prevented you from hiding mainMoon MC, otherwise your other listener will never act, now you are using parent object to listen to events, and can safely remove parts of your moon.
Just for a custom button you are going too much complex way. one movieClip is enough for create the button. First create a movieClip and inside that movieClip's timeline create two more frame for 'hover' and click effect. here is little bit of code to start.
var myButton_btn:CustomButton = new CustomButton();
addChild(myButton_btn);
myButton_btn.x = 100;
myButton_btn.y = 100;
myButton_btn.addEventListener(MouseEvent.ROLL_OVER, onOver);
myButton_btn.addEventListener(MouseEvent.ROLL_OUT, onOut);
myButton_btn.addEventListener(MouseEvent.CLICK, onClick);
function onOver(event:MouseEvent):void {
//trace('over');
event.target.gotoAndStop('hover');
}
function onOut(event:MouseEvent):void {
//trace('normal');
event.target.gotoAndStop('normal');
}
function onClick(event:MouseEvent):void {
//trace('click');
event.target.gotoAndStop('click');
}

ActionScript3, moving objects of type movieclip

Here i am trying to create a new movieclip type object, which is moved when function mvBall is called. When i run the code i get this err: implicit coercion of a value with static type object to a possibly unrelated type flash.display:MovieClip. Later on i want to be able to make the ball bounce back when it colides with another object. I'm new to action script and don't really know how things work so any help would be appreciated. Here's the code:
private function frame(x:Event):void {
var ball:MovieClip = new MovieClip();
ball.addEventListener(Event.ENTER_FRAME, animate);
ball.graphics.beginFill(0xff0000);
ball.graphics.drawCircle(100, 100, 15);
ball.graphics.endFill();
stage.addChild(ball);
}
private function animate(ev:Event):void {
mvBall(ev.target);
}
private function mvBall(mc:MovieClip) {
mc.x += 10;
}
You need to cast the target to MovieClip
private function animate(ev:Event):void {
mvBall(ev.target as MovieClip);
}
With that said it is better to just have one ENTER_FRAME handler and animate your objects in there.
stage.addEventListener(Event.ENTER_FRAME, animate);
private function animate(ev:Event):void
{
mvBall(myBall);
//other object animations
}
You are getting this error because the target property of the Event class is of type object.
In order to not throw the error, you need to cast it as a MovieClip:
mvBall(ev.target as MovieClip);
or
myBall(MovieClip(ev.target));
Something else to consider, is the difference between an Events target and currentTarget properties. If you ball had multiple layers/object inside it (sprites or other movieClips), the target would be whichever one of those sub-elements had the mouse over it during the click. currentTarget refers to the object that you've attached the listener to. In your case they may be the same (if your ball doesn't have any movie clips inside it), but your code could have unexpected results if you have sub-movieClips inside your ball.

AS3 Event Race Condition

I'm having some trouble solving a race condition that exists when I instantiate new MovieClip classes with some custom events that fire back before I can store the instances in an associative object.
Main Class
var pages:Object = {
"page1":"page1",
"page2":"page2"
};
for(var pageName:String in pages)
{
pages[pageName] = buildPage(pageName, onReady);
}
function buildPage(pageName:String, onReady:Function)
{
var newPage:MovieClip = new (getDefinitionByName(pageClass) as Class)();
newPage.addEventListener("PAGE_READY", onReady);
newPage.dispatchEvent(new Event("PAGE_CREATE"));
return newPage;
}
function onReady(e:Event)
{
for(var pageName:String in pages)
{
trace(typeof pages[pageName]);
}
}
Page 1 & 2 Classes extends MovieClip
function pageX()
{
this.addEventListener("PAGE_CREATE",this.onCreate);
}
function onCreate(e:Event)
{
this.graphics.beginFill(0xFF0000);
this.graphics.drawRect(0,0,200x200);
this.graphics.endFill();
this.dispatchEvent(new Event("PAGE_READY"));
}
Unfortunatley when this all fires off I get:
string
string
object
string
The original string object still exists before I can store the MovieClip instance for later reference. Any suggestions on how to beat this?
The code you posted does not depict a standard class.
Is this code on the movieclip frames or in separate files?
But as far as posted code I can see your issue is with the PAGE_CREATE event firing before the object has been added to the array.
your are doing
newPage.dispatchEvent(new Event("PAGE_CREATE"));
before you return the object.
You did not post the full classes so I really can't help you rewrite your code but, from what you posted I would remove the buildPage method completely and put the code in loop then dispatch the event after the object has been added to the array.
Also, another reason, best practice would be not to call a function from a loopif that function does not have a lot of code in it.
for(var pageName:String in pages)
{
var newPage:MovieClip = new (getDefinitionByName(pageClass) as Class)();
newPage.addEventListener("PAGE_READY", onReady);
pages[pageName] = newPage;
newPage.dispatchEvent(new Event("PAGE_CREATE"));
}

Actionscript3: add and remove EventListeners (with dynamic name and dynamic variables)

I am making a boardgame in flash Action Script 3. Each position on the board is a buttons like this: button_1_1, button_1_2 etc. Whenever a character is selected you want to move it so the script has to add event listeners for positions around the selected unit
// This function adds or deletes an event listener
function listentoButton (isTrue:int, position_x:int, position_y:int):void {
var myFunction:Function = new Function;
myFunction = function ():void {userClickedPosition(position_x, position_y)};
if (isTrue == 1) {
this["button_position_"+(position_x)+"_"+(position_y)].addEventListener(MouseEvent.CLICK, myFunction);
} else {
this["button_position_"+(position_x)+"_"+(position_y)].removeEventListener(MouseEvent.CLICK, myFunction);
}
}
In the rest of the code I have:
function userClickedPosition(position_x:int, position_y:int)
it selects or deselect a unit
function selectUnit(position_x:int, position_y:int):
it uses the listentoButton(1) function to add 8 listeners (the positions around the clicked unit)
function deselectUnit(position_x:int, position_y:int):
it uses the listentoButton(0) function to delete 8 listeners (the positions around the clicked unit)
My question: adding eventlisteners is no problem but removing them dont seem to work? What did I do wrong?
When you go to remove the event, you are using a new instance of myFunction, not the same one you added it with. You either need to declare the function like you would any other function, and use the event args to examine the button's position like. I Think you want the stageX and stageY properties:
http://www.adobe.com/livedocs/flex/3/langref/flash/events/MouseEvent.html
// This function adds or deletes an event listener
function listentoButton (isTrue:int, position_x:int, position_y:int):void {
if (isTrue == 1) {
this["button_position_"+(position_x)+"_"+(position_y)].addEventListener(MouseEvent.CLICK, myFunction);
} else {
this["button_position_"+(position_x)+"_"+(position_y)].removeEventListener(MouseEvent.CLICK, myFunction);
}
}
function myFunction(eventArg:MouseEvent):void {
//use MouseEvent
};
Or you can create a little MyFunctionParameters class to hold the coordinate information and create a new instance of that class, add it to a collection indexed by the x and y coordinates, and later when you go to remove the event, you would lookup the MySpaceParameters instance in the collection, based on x and y coordinates, then use that to remove function.
class MyFunctionParameters
{
public x:int;
public y:int;
function myFunction(eventArg:MouseEvent):void {
userClickedPosition(x,y);
};
}