AS3: Scale movieclip at another movieclips position? - actionscript-3

I'm trying to scale a base image based on a movieclip's x and y position? The base image image is also a MC.
infoIconCompFit.addEventListener(MouseEvent.ROLL_OVER, zoomInCompFit);
infoIconCompFit.addEventListener(MouseEvent.ROLL_OUT, zoomOutCompFit);
function zoomInCompFit(event:MouseEvent):void {
TweenLite.to(baseImage, 1, {scaleX:2, scaleY:2});
}
function zoomOutCompFit(event:MouseEvent):void {
TweenLite.to(baseImage, 1, {scaleX:1, scaleY:1});
}
What I mean is; is it possible to scale a movieclip at another movieclip's x and y position on the stage? Like I want the base movieclip to scale (zoom in) at the position of another movieclip on Mouse ROLL_OVER then zoom out on Mouse ROLL_OUT.
I get it to where it zooms in and out on the handlers, but how do I get it to zoom at that position relative to the other MC?
(Before) http://www.marketingfanatics.co.za/images/exampleNormal.jpg
(After) http://www.marketingfanatics.co.za/images/exampleZoomedl.jpg

Yes you can. But you need to write some code and have in mind where is transformation pivot of those Objects.
/**
* Calculate position and dimensions of image to zoom.
* #param img - image to animate, need to have transformation pivot in top left corner!
* #param point - point to zoom in (or null if zoom out) that will be new center of image
* #param scale - scale in zoom in
* #param viewWidth - container width
* #param viewHeight - container height
* #return object for tween engine with parameters to animate
*/
private function centerOn(img:DisplayObject, point:Point=null, scale:Number=2, viewWidth:Number=300, viewHeight:Number=200):Object
{
var r:Object;
var mm:Matrix = img.transform.matrix;
img.transform.matrix = new Matrix();
if (point == null) { // oryginal size
r = { x: 0, y: 0, width: img.width, height: img.height };
}else { // zoom
img.scaleX = img.scaleY = scale;
point.x *= scale;
point.y *= scale;
img.x = -point.x + viewWidth / 2;
img.y = -point.y + viewHeight / 2;
r = { x: img.x, y: img.y, width: img.width, height: img.height };
}
img.transform.matrix = mm;
return r;
}
Use example:
TweenLite.to(baseImage, 1, centerOn(baseIMG, new Point(100, 150))); //zoom in
TweenLite.to(baseImage, 1, centerOn(baseIMG)); //zoom out
centerOn(img, new Point(200, 135), 4, stage.stageWidth, stage.stageHeight); // to fit stage size
Remember that your Display Object is not masked and sometimes (zoom near borders) you will see scene under it!
PS. Code tested.

Related

LibGdx Issue of coordinates

I have issue in my LibGdx program. I gave 800 height and 480 width to my camera. I am drawing target under coordinates :
randomTargetX = new Random().nextInt((350 - 100) + 1) + 100;
randomTargetY = new Random().nextInt((600 - 300) + 1) + 300;
But after clicking on target my cannonball don't overlap target rectangle.
I am doing this in Touch:
if (Gdx.input.justTouched()) {
touchX = Gdx.input.getX();
touchY = Gdx.input.getY();
camera.unproject(touch.set(touchX, touchY, 0));
if (touch.y>200) {
isTouched = true;
rectangleCannonBall.x = (width / 2) - 50 / 2;
rectangleCannonBall.y = 0;
double angle = 180.0 / Math.PI * Math.atan2(rectangleCannonBall.x - touch.x, touch.y - rectangleCannonBall.y);
spriteCannon.setRotation((float) angle);
}
}
Doesn't work.
It's a cannonball game:
First i am setting camera.
Randomly showing targets inside range of coordinates.
On touch unprojecting camera with Vector3 new position.
On touch calculating target position with cannon position and getting angle to rotate cannon.
After rotating cannon I fire ball towards target.
Now when I do Rectanglar1.overlaps(rec2) , it doesn't work because of both rectangles have different points but by visible both overlaps each other.
When I check coordinates of Rectangle of Target and Touch its different.
The line:
camera.unproject(touch.set(touchX, touchY, 0));
Doesn't do anything.
Try with:
touch = camera.unproject(touch.set(touchX, touchY, 0));

HTML Canvas Rotating a character's gun to face mouse

I'm very new to Javascript and I've started a simple game. I want the character's gun to rotate to follow the mouse. So far, movement and everything else works fine, except that when I added the rotation functionality the character seems to rotate in a huge circle around the screen. Here's the jsfiddle: https://jsfiddle.net/jvwr8bug/#
function getMousePos(canvas, evt) {
var rect = canvas.getBoundingClientRect();
var mouseX = evt.clientX - rect.top;
var mouseY = evt.clientY - rect.left;
return {
x: mouseX,
y: mouseY
};
}
window.addEventListener('load', function() {
canvas.addEventListener('mousemove', function(evt) {
var m = getMousePos(canvas, evt);
mouse.x = m.x;
mouse.y = m.y;
}, false);
}, false);
The error seems to be somewhere there but obviously it could be something else
**Edit: Thanks to Blindman67 for the fix.
You were rotating the current transform by rotation each frame. ctx.rotate(a) rotates the current transform so each time it is called you increase the rotation amount by a. Think of it as a relative rotation rather than setting the absolute rotation.
To fix your code replace the canon rendering with
//cannon
//ctx.rotate(rotation); // << you had
// setTransform overwrites the current transform with a new one
// The arguments represent the vectors for the X and Y axis
// And are simply the direction and length of one pixel for each axis
// And a coordinate for the origin.
// All values are in screen/canvas pixels coordinates
// setTransform(xAxisX, xAxisY, yAxisX, yAxisY, originX, originY)
ctx.setTransform(1,0,0,1,x,y); // set center of rotation (origin) to center of gun
ctx.rotate(rotation); // rotate about that point.
ctx.fillStyle = "#989898";
ctx.fillRect(15, - 12.5, 25, 25); // draw relative to origin
ctx.lineWidth = 2;
ctx.strokeStyle = "#4f4f4f";
ctx.strokeRect( 15,- 12.5, 25, 25); // draw relative to origin
//body
ctx.fillStyle = "#5079c4";
ctx.beginPath();
ctx.arc(0, 0, size, 0, Math.PI * 2); // draw relative to origin
ctx.fill();
ctx.stroke();
// can't leave the transformed state as is because that will effect anything else
// that will be rendered. So reset to the default.
ctx.setTransform(1,0,0,1,0,0); // restore the origin to the default
And a few more problems to get it working
Just above rendering the canon get the direction to the mouse
// you had coordinates mixed up
// rotation = Math.atan2(mouse.x - y, mouse.y - x); // you had (similar)
rotation = Math.atan2(mouse.y - y, mouse.x - x);
And your mouse event listener is mixing up coordinates and not running very efficiently
Replace all your mouse code with. You don't need onload as the canvas already exists.
canvas.addEventListener('mousemove', function(evt) {
var rect = this.getBoundingClientRect();
mouse.x = evt.clientX - rect.left; // you had evt.clientX - rect.top
mouse.y = evt.clientY - rect.top; // you had evt.clientY - rect.left
}, false);

How to find correct offset to adjust sprite to the position of box2D body after rotation

I am trying to implement phsyics with the as3 box2d port. I currently have a b2body for each of some certain sprites in my game and I am able to update the sprite's positions correctly from the positions of the bodies. This is shown in the picture below (debugDraw shows the positions of the b2bodies overlaid on their corresponding spirtes. The green rectangles are the walls and floor)
However, I also want to have the sprite's rotations reflect the rotations of the b2bodies. But, after I rotate the sprites, the offset I use to center them correctly with the b2body positions is no longer accurate.
My code for updating the sprites positions is as follows:
private function Update(update_event:TimerEvent):void
{
//step physics simulation forward
world.Step(0.025,10,10);
//update all objects in world
for each (var obj:HouseItemPhysicsObject in physicsObjects)
{
//update object's position from gravity if it is not being dragged
if(!obj.isHeld)
{
/*adjust rotation of sprite along with body -> yourMC.rotation = (yourMCbody.GetAngle() * 180 / Math.PI) % 360; */
obj.object.rotation = (obj.pBody.GetAngle() * 180/Math.PI) % 360;
if(obj.object.rotation >=5)
// set object's x position but adjust for offset between the cooridinate systems
obj.x = (obj.pBody.GetPosition().x* scaleFactor)-(obj.object.width/2);
//keep in horizontal bounds of screen
if(obj.x > GeneralConstants.GAME_WIDTH)
{
obj.x =GeneralConstants.GAME_WIDTH;
}
else if(obj.x < 0)
{
obj.x = 0;
}
// set object's x position but adjust for offset between the cooridinate systems in Flash and box2d
obj.y = (obj.pBody.GetPosition().y * scaleFactor)-(obj.object.height/2);
//keep in vertical bounds of the screen
if(obj.y > GeneralConstants.GAME_HEIGHT)
{
obj.y =GeneralConstants.GAME_HEIGHT;
}
else if(obj.x < 0)
{
obj.x = 0;
}
/*Draw shapes to see for debug*/
//obj.DrawDebug();
//trace("OBJECT's X is :" + obj.x + " Y is :" +obj.y);
trace("Object's rotation is:" + obj.object.rotation);
}
}
//move debug draw to front of display list
m_sprite.parent.setChildIndex(m_sprite, m_sprite.parent.numChildren - 5);
world.DrawDebugData();
}
How can I find the correct X and Y offset between the coordinate systems (Flash and Box2d) after rotating the sprite according to the b2Body? Thanks for the help.
EDIT:
For clarity, the object is a class that extends the Sprite class, and it's data member _object is a an instance of MovieClip.
Box2D objects have their anchor point in the center by default, while for Flash objects, it's in the top left. To position them properly, you need to take this into account
Easy way
Wrap your Bitmaps/whatever in a Sprite and center them:
// create the image, center it, and add it to a holder Sprite
var image:Bitmap = new Bitmap( objGraphicsBitmapData );
image.x = -image.width * 0.5;
image.y = -image.height * 0.5;
var holder:Sprite = new Sprite;
holder.addChild( image );
Now just set the position and rotation of holder as you do currently, and it should be fine
Hard way
You need to manually adjust the position offset based on the object's rotation. A simple rotation function:
public function rotate( p:Point, radians:Number, out:Point = null ):Point
{
// formula is:
// x1 = x * cos( r ) - y * sin( r )
// y1 = x * sin( r ) + y * cos( r )
var sin:Number = Math.sin( radians );
var cos:Number = Math.cos( radians );
var ox:Number = p.x * cos - p.y * sin;
var oy:Number = p.x * sin + p.y * cos;
// we use ox and oy in case out is one of our points
if ( out == null )
out = new Point;
out.x = ox;
out.y = oy;
return out;
}
First we need to store the object's offset - this is normally new Point( -obj.width * 0.5, -obj.height * 0.5 ). You need to stock this while it's rotation is 0, and rotating the object will change its width and height properties, so the following won't work properly.
obj.offset = new Point( -obj.width * 0.5, -obj.height * 0.5 );
When you're updating the position, simply rotate the offset by the rotation and add it:
// get our object's position and rotation
// NOTE: you'll probably need to adjust the position based on your pixels per meter value
var pos:Point = new Point( obj.pBody.GetPosition().x, obj.pBody.GetPosition().y ); // pos in screen coords
var rotR:Number = obj.pBody.GetAngle(); // rotation in radians
var rotD:Number = radiansToDegrees( rotR ); // rotation in degrees
// rotate our offset by our rotation
var offset:Point = rotate( obj.offset, rotR );
// set our position and rotation
obj.x = pos.x + offset.x;
obj.y = pos.y + offset.y;
obj.rotation = rotD;
Other useful functions:
public function degreesToRadians( deg:Number ):Number
{
return deg * ( Math.PI / 180.0 );
}
public function radiansToDegrees( rad:Number ):Number
{
return rad * ( 180.0 / Math.PI );
}
If you do it to give your sprites properties of physical objects, it can be easier to use physInjector for box2D:
http://www.emanueleferonato.com/2013/03/27/add-box2d-physics-to-your-projects-in-a-snap-with-physinjector/
It is free can do it in a couple of lines.

Insane Graphics.lineStyle behavior

I'd like some help with a little project of mine.
Background:
i have a little hierarchy of Sprite derived classes (5 levels starting from the one, that is the root application class in Flex Builder). Width and Height properties are overriden so that my class always remembers it's requested size (not just bounding size around content) and also those properties explicitly set scaleX and scaleY to 1, so that no scaling would ever be involved. After storing those values, draw() method is called to redraw content.
Drawing:
Drawing is very straight forward. Only the deepest object (at 1-indexed level 5) draws something into this.graphics object like this:
var gr:Graphics = this.graphics;
gr.clear();
gr.lineStyle(0, this.borderColor, 1, true, LineScaleMode.NONE);
gr.beginFill(0x0000CC);
gr.drawRoundRectComplex(0, 0, this.width, this.height, 10, 10, 0, 0);
gr.endFill();
Further on:
There is also MouseEvent.MOUSE_WHEEL event attached to the parent of the object that draws. What handler does is simply resizes that drawing object.
Problem:
Screenshot
When resizing sometimes that hairline border line with LineScaleMode.NONE set gains thickness (quite often even >10 px) + it quite often leaves a trail of itself (as seen in the picture above and below blue box (notice that box itself has one px black border)). When i set lineStile thickness to NaN or alpha to 0, that trail is no more happening.
I've been coming back to this problem and dropping it for some other stuff for over a week now.
Any ideas anyone?
P.S. Grey background is that of Flash Player itself, not my own choise.. :D
As requested, a bit more:
Application is supposed to be a calendar-timeline with a zooming "feature" (project for a course at university). Thus i have these functions that have something to do with resizing:
public function performZoom():void
{
// Calculate new width:
var newDayWidth:Number = view.width / 7 * this.calModel.zoom;
if (newDayWidth < 1)
{
newDayWidth = 1;
}
var newWidth:int = int(newDayWidth * timeline.totalDays);
// Calculate day element Height/Width ratio:
var headerHeight:Number = this.timeline.headerAllDay;
var proportion:Number = 0;
if (this.view.width != 0 && this.view.height != 0)
{
proportion = 1 / (this.view.width / 7) * (this.view.height - this.timeline.headerAllDay);
}
// Calculate new height:
var newHeight:int = int(newDayWidth * proportion + this.timeline.headerAllDay);
// Calculate mouse position scale on X axis:
var xScale:Number = 0;
if (this.timeline.width != 0)
{
xScale = newWidth / this.timeline.width;
}
// Calculate mouse position scale on Y axis:
var yScale:Number = 0;
if (this.timeline.height - this.timeline.headerAllDay != 0)
{
yScale = (newHeight - this.timeline.headerAllDay) / (this.timeline.height - this.timeline.headerAllDay);
}
this.timeline.beginUpdate();
// Resize the timeline
this.timeline.resizeElement(newWidth, newHeight);
this.timeline.endUpdate();
// Move timeline:
this.centerElement(xScale, yScale);
// Reset timeline local mouse position:
this.centerMouse();
}
public function resizeElement(widthValue:Number, heightValue:Number):void
{
var prevWidth:Number = this.myWidth;
var prevHeight:Number = this.myHeight;
if (widthValue != prevWidth || heightValue != prevHeight)
{
super.width = widthValue;
super.height = heightValue;
this.scaleX = 1.0;
this.scaleY = 1.0;
this.myHeight = heightValue;
this.myWidth = widthValue;
if (!this.docking)
{
this.origHeight = heightValue;
this.origWidth = widthValue;
}
this.updateAnchorMargins();
onResizeInternal(prevWidth, prevHeight, widthValue, heightValue);
}
}
Yes. I know.. a lot of core, and a lot of properties, but in fact most of the stuff has been disabled at the end and the situation is as described at the top.
this didn't work:
gr.lineStyle(); // Reset line style
Can we see your resizing code?
Also try clearing your line style as well as your fill:
gr.lineStyle(0, this.borderColor, 1, true, LineScaleMode.NONE);
gr.beginFill(0x0000CC);
gr.drawRoundRectComplex(0, 0, this.width, this.height, 10, 10, 0, 0);
gr.endFill();
gr.lineStyle();//<---- add this line
I don't know whether it's flash bug or what it is, but i finally found the solution.
The thing is that in my case when resizing in a nutshell you get like this:
this.width = this.stage.stageWidth / 7 * 365;
When i switch to maximized window this.width gains value 86k+. When i added this piece of code to draw horizontal line, it fixed itself:
var recWidth:Number = this.width;
var i:int;
var newEnd:Number = 0;
for (i = 1; newEnd < recWidth; i++)
{
newEnd = 100 * i;
if (newEnd > recWidth)
{
newEnd = recWidth;
}
gr.lineTo(newEnd, 0);
}
I don't know what is going on.. This is inconvenient...

Optimizing my dynamic background engine for a 2d flash game in actionscript-3

Edit 2: judging on the lack of replies I start wondering if my issue is clear enough. Please tell me if I need to elaborate more.
Notice: see bottom for a code update!
Short introduction: I'm writing a 2 dimensional flash space game in actionscript. The universe is infinitely big, because of this feature, the background has to be rendered dynamically and background objects (gas clouds, stars, etc.) have to be positioned randomly.
I created a class called BackgroundEngine and it's working very well, the problem is however the rendering performance. This is how it works:
At startup, 4 background containers (each the size of the stage) are created around the player. Top left, top right, bottom left and bottom right. All background squares are added to a master container, for easy movement of the background. Now, there are 2 polling functions:
1) "garbage poller": looks for background containers that are 2 times the stage width or height away from the player's X or Y coord, respectively. If so, it will remove that background square and allow it for garbage collection.
2) "rendering poller": looks whether there is currently a background at all sides of the player (x - stageWidth, x + stageWidth, y - stageHeight, y + stageHeight). If not, it will draw a new background square at the corresponding location.
All background squares are created with the following function (the ones that are created dynamically and the four on startup):
<<< removed old code, see bottom for updated full source >>>
All the randoms you see there are making sure that the environment looks very unique on every square. This actually works great, the universe looks quite awesome.
The following assets are being used as background objects:
1) Simple stars : http://www.feedpostal.com/client/assets/background/1.png (you probably won't be able to see that one in a browser with a white background).
2) Bright stars : http://www.feedpostal.com/client/assets/background/2.png
3) White gas clouds : http://www.feedpostal.com/client/assets/background/3.png
4) Red gas clouds: http://www.feedpostal.com/client/assets/background/4.png
Important notes:
1) All assets are cached, so they don't have to be re-downloaded all the time. They are only downloaded once.
2) The images are not rotating or being scaled after they are created, so I enabled cacheAsBitmap for all objects, containers and the masterContainer.
3) I had to use PNG formats in Photoshop because GIFs did not seem to be rendered very well in flash when used with transparency.
So, the problem is that when I fly around the rendering of the background takes too much performance: the client starts "lagging" (FPS wise). Because of this, I need to optimize the background engine so that it will render much quicker. Can you folks help me out here?
Update 1:
This is what I have so far after the one response I got.
BackgroundEngine.as
package com.tommedema.background
{
import br.com.stimuli.loading.BulkLoader;
import com.tommedema.utils.Settings;
import com.tommedema.utils.UtilLib;
import flash.display.Bitmap;
import flash.display.BitmapData;
import flash.display.Sprite;
import flash.events.Event;
public final class BackgroundEngine extends Sprite
{
private static var isLoaded:Boolean = false;
private static var bulkLoader:BulkLoader = BulkLoader.getLoader("main");
private static var masterContainer:Sprite;
private static var containers:Array = [];
private static var stageWidth:uint;
private static var stageHeight:uint;
private static var assets:Array;
//moves the background's X coord
public static function moveX(amount:Number):void
{
if (masterContainer)
{
masterContainer.x += amount;
collectGarbage();
drawNextContainer();
}
}
//moves the background's Y coord
public static function moveY(amount:Number):void
{
if (masterContainer)
{
masterContainer.y += amount;
collectGarbage();
drawNextContainer();
}
}
//returns whether the background engine has been loaded already
public static function loaded():Boolean
{
return isLoaded;
}
//loads the background engine
public final function load():void
{
//set stage width and height
stageWidth = stage.stageWidth;
stageHeight = stage.stageHeight;
//retreive all background assets
bulkLoader.add(Settings.ASSETS_PRE_URL + "background/1.png", {id: "background/1.png"});
bulkLoader.add(Settings.ASSETS_PRE_URL + "background/2.png", {id: "background/2.png"});
bulkLoader.add(Settings.ASSETS_PRE_URL + "background/3.png", {id: "background/3.png"});
bulkLoader.add(Settings.ASSETS_PRE_URL + "background/4.png", {id: "background/4.png"});
bulkLoader.addEventListener(BulkLoader.COMPLETE, assetsComplete);
bulkLoader.start();
//set isLoaded to true
isLoaded = true;
}
//poller function for drawing next background squares
private static function drawNextContainer():void
{
var stageCenterX:Number = stageWidth / 2;
var stageCenterY:Number = stageHeight / 2;
var curContainer:Bitmap = hasBackground(stageCenterX, stageCenterY);
if (curContainer)
{
//top left
if (!hasBackground(stageCenterX - stageWidth, stageCenterY - stageHeight)) drawNewSquare(curContainer.x - stageWidth, curContainer.y - stageHeight);
//top
if (!hasBackground(stageCenterX, stageCenterY - stageHeight)) drawNewSquare(curContainer.x, curContainer.y - stageHeight);
//top right
if (!hasBackground(stageCenterX + stageWidth, stageCenterY - stageHeight)) drawNewSquare(curContainer.x + stageWidth, curContainer.y - stageHeight);
//center left
if (!hasBackground(stageCenterX - stageWidth, stageCenterY)) drawNewSquare(curContainer.x - stageWidth, curContainer.y);
//center right
if (!hasBackground(stageCenterX + stageWidth, stageCenterY)) drawNewSquare(curContainer.x + stageWidth, curContainer.y);
//bottom left
if (!hasBackground(stageCenterX - stageWidth, stageCenterY + stageHeight)) drawNewSquare(curContainer.x - stageWidth, curContainer.y + stageHeight);
//bottom
if (!hasBackground(stageCenterX, stageCenterY + stageHeight)) drawNewSquare(curContainer.x, curContainer.y + stageHeight);
//bottom right
if (!hasBackground(stageCenterX + stageWidth, stageCenterY + stageHeight)) drawNewSquare(curContainer.x + stageWidth, curContainer.y + stageHeight);
}
}
//draws the next square and adds it to the master container
private static function drawNewSquare(x:Number, y:Number):void
{
containers.push(genSquareBg());
var cIndex:uint = containers.length - 1;
containers[cIndex].x = x;
containers[cIndex].y = y;
masterContainer.addChild(containers[cIndex]);
}
//returns whether the given location has a background and if so returns the corresponding square
private static function hasBackground(x:Number, y:Number):Bitmap
{
var stageX:Number;
var stageY:Number;
for(var i:uint = 0; i < containers.length; i++)
{
stageX = masterContainer.x + containers[i].x;
stageY = masterContainer.y + containers[i].y;
if ((containers[i]) && (stageX < x) && (stageX + stageWidth > x) && (stageY < y) && (stageY + stageHeight > y)) return containers[i];
}
return null;
}
//polling function for old background squares garbage collection
private static function collectGarbage():void
{
var stageX:Number;
var stageY:Number;
for(var i:uint = 0; i < containers.length; i++)
{
if (containers[i])
{
stageX = masterContainer.x + containers[i].x;
stageY = masterContainer.y + containers[i].y;
if ((stageX < -stageWidth * 1.5) || (stageX > stageWidth * 2.5) || (stageY < -stageHeight * 1.5) || (stageY > stageHeight * 2.5))
{
containers[i].parent.removeChild(containers[i]);
containers.splice(i, 1);
}
}
}
}
//dispatched when all assets have finished downloading
private final function assetsComplete(event:Event):void
{
assets = [];
assets.push(bulkLoader.getBitmap("background/1.png")); //star simple
assets.push(bulkLoader.getBitmap("background/2.png")); //star bright
assets.push(bulkLoader.getBitmap("background/3.png")); //cloud white
assets.push(bulkLoader.getBitmap("background/4.png")); //cloud red
init();
}
//initializes startup background containers
private final function init():void
{
masterContainer = new Sprite(); //create master container
//generate default background containers
containers.push(genSquareBg()); //top left
containers[0].x = 0;
containers[0].y = 0;
containers.push(genSquareBg()); //top
containers[1].x = stageWidth;
containers[1].y = 0;
containers.push(genSquareBg()); //top right
containers[2].x = stageWidth * 2;
containers[2].y = 0;
containers.push(genSquareBg()); //center left
containers[3].x = 0;
containers[3].y = stageHeight;
containers.push(genSquareBg()); //center
containers[4].x = stageWidth;
containers[4].y = stageHeight;
containers.push(genSquareBg()); //center right
containers[5].x = stageWidth * 2;
containers[5].y = stageHeight;
containers.push(genSquareBg()); //bottom left
containers[6].x = 0;
containers[6].y = stageHeight * 2;
containers.push(genSquareBg()); //bottom
containers[7].x = stageWidth;
containers[7].y = stageHeight * 2;
containers.push(genSquareBg()); //bottom right
containers[8].x = stageWidth * 2;
containers[8].y = stageHeight * 2;
//add the new containers to the master container
for (var i:uint = 0; i <= containers.length - 1; i++)
{
masterContainer.addChild(containers[i]);
}
//display the master container
masterContainer.x = 0 - stageWidth;
masterContainer.y = 0 - stageHeight;
masterContainer.cacheAsBitmap = true;
addChild(masterContainer);
}
//duplicates a bitmap display object
private static function dupeBitmap(source:Bitmap):Bitmap {
var data:BitmapData = source.bitmapData;
var bitmap:Bitmap = new Bitmap(data);
return bitmap;
}
//draws a simple star
private static function drawStar(x:Number, y:Number, width:uint, height:uint):Sprite
{
var creation:Sprite = new Sprite();
creation.graphics.lineStyle(1, 0xFFFFFF);
creation.graphics.beginFill(0xFFFFFF);
creation.graphics.drawRect(x, y, width, height);
return creation;
}
//generates a background square
private static function genSquareBg():Bitmap
{
//set 1% margin
var width:Number = stageWidth * 0.99;
var height:Number = stageHeight * 0.99;
var startX:Number = 0 + stageWidth / 100;
var startY:Number = 0 + stageHeight / 100;
var scale:Number;
var drawAmount:uint;
var tmpBitmap:Bitmap;
var tmpSprite:Sprite;
var i:uint;
//create container
var container:Sprite = new Sprite();
//draw simple stars
drawAmount = UtilLib.getRandomInt(100, 250);
for(i = 1; i <= drawAmount; i++)
{
tmpSprite = drawStar(0, 0, 1, 1);
tmpSprite.x = UtilLib.getRandomInt(0, stageWidth);
tmpSprite.y = UtilLib.getRandomInt(0, stageHeight);
tmpSprite.alpha = UtilLib.getRandomInt(3, 10) / 10;
scale = UtilLib.getRandomInt(2, 10) / 10;
tmpSprite.scaleX = tmpSprite.scaleY = scale;
container.addChild(tmpSprite);
}
//draw bright stars
if (Math.random() >= 0.8) drawAmount = UtilLib.getRandomInt(1, 2);
else drawAmount = 0;
for(i = 1; i <= drawAmount; i++)
{
tmpBitmap = dupeBitmap(assets[1]);
tmpBitmap.alpha = UtilLib.getRandomInt(3, 7) / 10;
tmpBitmap.rotation = UtilLib.getRandomInt(0, 360);
scale = UtilLib.getRandomInt(3, 10) / 10;
tmpBitmap.scaleX = scale; tmpBitmap.scaleY = scale;
tmpBitmap.x = UtilLib.getRandomInt(startX + tmpBitmap.width, width - tmpBitmap.width);
tmpBitmap.y = UtilLib.getRandomInt(startY + tmpBitmap.height, height - tmpBitmap.height);
container.addChild(tmpBitmap);
}
//draw white clouds
drawAmount = UtilLib.getRandomInt(1, 4);
for(i = 1; i <= drawAmount; i++)
{
tmpBitmap = dupeBitmap(assets[2]);
tmpBitmap.alpha = UtilLib.getRandomInt(1, 10) / 10;
scale = UtilLib.getRandomInt(15, 30);
tmpBitmap.scaleX = scale / 10;
tmpBitmap.scaleY = UtilLib.getRandomInt(scale / 2, scale) / 10;
tmpBitmap.x = UtilLib.getRandomInt(startX, width - tmpBitmap.width);
tmpBitmap.y = UtilLib.getRandomInt(startY, height - tmpBitmap.height);
container.addChild(tmpBitmap);
}
//draw red clouds
drawAmount = UtilLib.getRandomInt(0, 1);
for(i = 1; i <= drawAmount; i++)
{
tmpBitmap = dupeBitmap(assets[3]);
tmpBitmap.alpha = UtilLib.getRandomInt(2, 6) / 10;
scale = UtilLib.getRandomInt(5, 30) / 10;
tmpBitmap.scaleX = scale; tmpBitmap.scaleY = scale;
tmpBitmap.x = UtilLib.getRandomInt(startX, width - tmpBitmap.width);
tmpBitmap.y = UtilLib.getRandomInt(startY, height - tmpBitmap.height);
container.addChild(tmpBitmap);
}
//convert all layers to a single bitmap layer and return
var bitmapData:BitmapData = new BitmapData(stageWidth, stageHeight, true, 0x000000);
bitmapData.draw(container);
container = null;
var bitmapContainer:Bitmap = new Bitmap(bitmapData);
bitmapContainer.cacheAsBitmap = true;
return bitmapContainer;
}
}
}
When the player is moving, the background moveX and moveY methods are called with the inverse direction of the player. This will also cause the collectGarbage and drawNextContainer methods to be called.
The problem with this setup is that there are a minimum of 9 containers active at all times. Top left, top, top right, center left, center, center right, bottom left, bottom and bottom right. This takes a lot of performance.
Edit: I also wonder, should I use cacheAsBitmap? If so, on which images? On the containers and the master container or on only one of them? When I enable it for all images (even the temporary sprite objects) it's actually lagging more.
Update 2:
This version is using squares that are twice as big as the stage. Only one or two squares should be loaded at a time. It is better, but I still notice a performance hit while moving. It makes the client freeze for a very brief moment. Any idea how to optimize it?
BackgroundEngine2.as
package com.tommedema.background
{
import br.com.stimuli.loading.BulkLoader;
import com.tommedema.utils.Settings;
import com.tommedema.utils.UtilLib;
import flash.display.Bitmap;
import flash.display.BitmapData;
import flash.display.Sprite;
import flash.events.Event;
public final class BackgroundEngine2 extends Sprite
{
//general
private static var isLoaded:Boolean = false;
private static var bulkLoader:BulkLoader = BulkLoader.getLoader("main");
private static var assets:Array;
//objects
private static var masterContainer:Sprite;
private static var containers:Array = [];
//stage
private static var stageWidth:uint;
private static var stageHeight:uint;
private static var stageCenterX:Number;
private static var stageCenterY:Number;
//moves the background's X coord
public static function moveX(amount:Number):void
{
if (!masterContainer) return;
masterContainer.x += amount;
collectGarbage();
drawNextContainer();
}
//moves the background's Y coord
public static function moveY(amount:Number):void
{
if (!masterContainer) return;
masterContainer.y += amount;
collectGarbage();
drawNextContainer();
}
//returns whether the background engine has been loaded already
public static function loaded():Boolean
{
return isLoaded;
}
//loads the background engine
public final function load():void
{
//set stage width, height and center
stageWidth = stage.stageWidth;
stageHeight = stage.stageHeight;
stageCenterX = stageWidth / 2;
stageCenterY = stageHeight / 2;
//retreive background assets
bulkLoader.add(Settings.ASSETS_PRE_URL + "background/1.png", {id: "background/1.png"});
bulkLoader.add(Settings.ASSETS_PRE_URL + "background/2.png", {id: "background/2.png"});
bulkLoader.add(Settings.ASSETS_PRE_URL + "background/3.png", {id: "background/3.png"});
bulkLoader.add(Settings.ASSETS_PRE_URL + "background/4.png", {id: "background/4.png"});
bulkLoader.addEventListener(BulkLoader.COMPLETE, assetsComplete);
bulkLoader.start();
//set isLoaded to true
isLoaded = true;
}
//poller function for drawing next background squares
private static function drawNextContainer():void
{
var curContainer:Bitmap = hasBackground(stageCenterX, stageCenterY);
if (curContainer)
{
if (!hasBackground(stageCenterX - stageWidth * 0.75, stageCenterY - stageHeight * 0.75)) //top left
drawNewSquare(curContainer.x - curContainer.width, curContainer.y - curContainer.height);
if (!hasBackground(stageCenterX, stageCenterY - stageHeight * 0.75)) //top
drawNewSquare(curContainer.x, curContainer.y - curContainer.height);
if (!hasBackground(stageCenterX + stageWidth * 0.75, stageCenterY - stageHeight * 0.75)) //top right
drawNewSquare(curContainer.x + curContainer.width, curContainer.y - curContainer.height);
if (!hasBackground(stageCenterX - stageWidth * 0.75, stageCenterY)) //center left
drawNewSquare(curContainer.x - curContainer.width, curContainer.y);
if (!hasBackground(stageCenterX + stageWidth * 0.75, stageCenterY)) //center right
drawNewSquare(curContainer.x + curContainer.width, curContainer.y);
if (!hasBackground(stageCenterX - stageWidth * 0.75, stageCenterY + stageHeight * 0.75)) //bottom left
drawNewSquare(curContainer.x - curContainer.width, curContainer.y + curContainer.height);
if (!hasBackground(stageCenterX, stageCenterY + stageHeight * 0.75)) //bottom center
drawNewSquare(curContainer.x, curContainer.y + curContainer.height);
if (!hasBackground(stageCenterX + stageWidth * 0.75, stageCenterY + stageHeight * 0.75)) //bottom right
drawNewSquare(curContainer.x + curContainer.width, curContainer.y + curContainer.height);
}
}
//draws the next square and adds it to the master container
private static function drawNewSquare(x:Number, y:Number):void
{
containers.push(genSquareBg());
var cIndex:uint = containers.length - 1;
containers[cIndex].x = x;
containers[cIndex].y = y;
masterContainer.addChild(containers[cIndex]);
}
//returns whether the given location has a background and if so returns the corresponding square
private static function hasBackground(x:Number, y:Number):Bitmap
{
var stageX:Number;
var stageY:Number;
for(var i:uint = 0; i < containers.length; i++)
{
stageX = masterContainer.x + containers[i].x;
stageY = masterContainer.y + containers[i].y;
if ((containers[i]) && (stageX < x) && (stageX + containers[i].width > x) && (stageY < y) && (stageY + containers[i].height > y)) return containers[i];
}
return null;
}
//polling function for old background squares garbage collection
private static function collectGarbage():void
{
var stageX:Number;
var stageY:Number;
for(var i:uint = 0; i < containers.length; i++)
{
if ((containers[i]) && (!isRequiredContainer(containers[i])))
{
masterContainer.removeChild(containers[i]);
containers.splice(i, 1);
}
}
}
//returns whether the given container is required for display
private static function isRequiredContainer(container:Bitmap):Boolean
{
if (hasBackground(stageCenterX, stageCenterY) == container) //center
return true;
if (hasBackground(stageCenterX - stageWidth * 0.75, stageCenterY - stageHeight * 0.75) == container) //top left
return true;
if (hasBackground(stageCenterX, stageCenterY - stageHeight * 0.75) == container) //top
return true;
if (hasBackground(stageCenterX + stageWidth * 0.75, stageCenterY - stageHeight * 0.75) == container) //top right
return true;
if (hasBackground(stageCenterX - stageWidth * 0.75, stageCenterY) == container) //center left
return true;
if (hasBackground(stageCenterX + stageWidth * 0.75, stageCenterY) == container) //center right
return true;
if (hasBackground(stageCenterX - stageWidth * 0.75, stageCenterY + stageHeight * 0.75) == container) //bottom left
return true;
if (hasBackground(stageCenterX, stageCenterY + stageHeight * 0.75) == container) //bottom center
return true;
if (hasBackground(stageCenterX + stageWidth * 0.75, stageCenterY + stageHeight * 0.75) == container) //bottom right
return true;
return false;
}
//dispatched when all assets have finished downloading
private final function assetsComplete(event:Event):void
{
assets = [];
assets.push(bulkLoader.getBitmap("background/1.png")); //star simple
assets.push(bulkLoader.getBitmap("background/2.png")); //star bright
assets.push(bulkLoader.getBitmap("background/3.png")); //cloud white
assets.push(bulkLoader.getBitmap("background/4.png")); //cloud red
init();
}
//initializes startup background containers
private final function init():void
{
masterContainer = new Sprite(); //create master container
//generate default background container
containers.push(genSquareBg()); //top left
containers[0].x = 0;
containers[0].y = 0;
masterContainer.addChild(containers[0]);
//display the master container
masterContainer.x = -(stageWidth / 2);
masterContainer.y = -(stageHeight / 2);
masterContainer.cacheAsBitmap = true;
addChild(masterContainer);
}
//duplicates a bitmap display object
private static function dupeBitmap(source:Bitmap):Bitmap {
var data:BitmapData = source.bitmapData;
var bitmap:Bitmap = new Bitmap(data);
return bitmap;
}
//draws a simple star
private static function drawStar(x:Number, y:Number, width:uint, height:uint):Sprite
{
var creation:Sprite = new Sprite();
creation.graphics.lineStyle(1, 0xFFFFFF);
creation.graphics.beginFill(0xFFFFFF);
creation.graphics.drawRect(x, y, width, height);
return creation;
}
//generates a background square
private static function genSquareBg():Bitmap
{
var width:Number = stageWidth * 2;
var height:Number = stageHeight * 2;
var startX:Number = 0;
var startY:Number = 0;
var scale:Number;
var drawAmount:uint;
var tmpBitmap:Bitmap;
var tmpSprite:Sprite;
var i:uint;
//create container
var container:Sprite = new Sprite();
//draw simple stars
drawAmount = UtilLib.getRandomInt(100, 250);
for(i = 1; i <= drawAmount; i++)
{
tmpSprite = drawStar(0, 0, 1, 1);
tmpSprite.x = UtilLib.getRandomInt(startX, width);
tmpSprite.y = UtilLib.getRandomInt(startY, height);
tmpSprite.alpha = UtilLib.getRandomInt(3, 10) / 10;
scale = UtilLib.getRandomInt(5, 15) / 10;
tmpSprite.scaleX = tmpSprite.scaleY = scale;
container.addChild(tmpSprite);
}
//draw bright stars
drawAmount = UtilLib.getRandomInt(1, 2);
for(i = 1; i <= drawAmount; i++)
{
tmpBitmap = dupeBitmap(assets[1]);
tmpBitmap.alpha = UtilLib.getRandomInt(3, 7) / 10;
tmpBitmap.rotation = UtilLib.getRandomInt(0, 360);
scale = UtilLib.getRandomInt(3, 10) / 10;
tmpBitmap.scaleX = scale; tmpBitmap.scaleY = scale;
tmpBitmap.x = UtilLib.getRandomInt(startX + tmpBitmap.width, width - tmpBitmap.width);
tmpBitmap.y = UtilLib.getRandomInt(startY + tmpBitmap.height, height - tmpBitmap.height);
container.addChild(tmpBitmap);
}
//draw white clouds
drawAmount = UtilLib.getRandomInt(2, 4);
for(i = 1; i <= drawAmount; i++)
{
tmpBitmap = dupeBitmap(assets[2]);
tmpBitmap.alpha = UtilLib.getRandomInt(1, 10) / 10;
scale = UtilLib.getRandomInt(15, 40);
tmpBitmap.scaleX = scale / 10;
tmpBitmap.scaleY = UtilLib.getRandomInt(scale / 2, scale * 2) / 10;
tmpBitmap.x = UtilLib.getRandomInt(startX, width - tmpBitmap.width);
tmpBitmap.y = UtilLib.getRandomInt(startY, height - tmpBitmap.height);
container.addChild(tmpBitmap);
}
//draw red clouds
drawAmount = UtilLib.getRandomInt(0, 2);
for(i = 1; i <= drawAmount; i++)
{
tmpBitmap = dupeBitmap(assets[3]);
tmpBitmap.alpha = UtilLib.getRandomInt(2, 6) / 10;
scale = UtilLib.getRandomInt(5, 40) / 10;
tmpBitmap.scaleX = scale; tmpBitmap.scaleY = scale;
tmpBitmap.x = UtilLib.getRandomInt(startX, width - tmpBitmap.width);
tmpBitmap.y = UtilLib.getRandomInt(startY, height - tmpBitmap.height);
container.addChild(tmpBitmap);
}
//convert all layers to a single bitmap layer and return
var bitmapData:BitmapData = new BitmapData(width, height, true, 0x000000);
bitmapData.draw(container);
container = null;
var bitmapContainer:Bitmap = new Bitmap(bitmapData);
//bitmapContainer.cacheAsBitmap = true;
return bitmapContainer;
}
}
}
ok, this should show you can really get another category of numbers with other aproaches ...
the limit here is not the number of stars, the limit is density, i.e. the number of stars visible at the same time ... with text disabled, i can get up to 700 # 30fps, on a Core2Duo, with quite a recent version of the debug player ...
i realized, flash player is not very good at clipping ... and that actually, using the most simple way, you spend a whole lot of time moving around objects, that are far from being visible ...
to really be able to optimize things, i chose to use MVC here ... not in the classic bloated way ... the idea is to handle the model, and if any elements are visible, create views for them ...
now the best aproach is to build up a spatial tree ...
you have leaves, containing objects, and nodes containing leaves or nodes
if you add an object to a leaf and it surpases a certain size, you turn it into a node with nxn leaves, redestributing its children between
any object added to the background will be added to a grid, determined by the object's coordinates ... grids are created just-in-time, an start off as leaves
the big advantage of this is, that you can quickly isolate the visible nodes/leaves.
in each iteration, only the nodes/leaves which either turn visible, or are already visible (and may become invisible), are interesting. you need not do any updates in the rest of the tree. after finding all the visible objects, you create views for objects that turn visible, update the position of those that simply stay visible, and destroy views for objects that become invisible ...
this saves an awful lot of everything ... memory and computation power ...
if you try with a huge world size (100000), you will see, that you run out of RAM quickly, long before CPU does anything ... instantiating 500000 stars uses 700MB ram, with about 50 stars visible, running at 70 fps without any tremendous CPU usage ...
the engine is just a proof of concept ... and code looks awful ... the only feature i am currently proud about is, that it supports object to occupate a certain area, which is why an object can be part of several leafs ... i think, this is something i will remove though, because it should give me a great speed up ... you also see, it stalls a little, while adding stars, which happens, when leafs flip to nodes ...
i am quite sure, this is not the best way for a spatial subdivision tree, it's just that everything i found seemed kind of useless to me ... probably someone who studied (and understood) CS, can refine my approach ... :)
other than that, i used an object pool for the views, and Haxe, since it performs better ... a thing that probably is not so clever, is to move all the views individually, instead of putting them on one layer and moving that around ...
some people also render things into BitmapDatas manually, to gain performance, which seems to work quite well (just can't find the question right now) ... you should however consider using copyPixels instead of draw ...
hope this helps ... ;)
edit: i decided to turn my detailed answer into a blog post ... have fun reading ... ;)
You may want to see if you can blit all of the pieces together into a flattend Bitmaps as you go. Draw all of the layers and then use BitmapData's draw method to combine them into a single Bitmap.
Even with cacheAsBitmap on for all of the pieces Flash is still having to combine all of those pieces every frame.
Try stretching the window of the player, both bigger and smaller. If that has a significant effect on frame rate, your fastest and easiest way to improve performance is to shrink the size of the stage. This tends to be an unpopular answer when presented to people - especially artists - but if your bottleneck is in the size of your stage, there is not much you can do in code to fix that.
What if instead of destroying background squares you just put them in a pile of "ready to go" squares that you can draw on, capping it at like 4? then you don't have to create one when you need one you just move it into the right spot and maybe shuffle the stars or something.
[would add example but i don't write AS3 code :(]
You should be able to simplify some of your math by using stored variables instead of stageCenterX + stageWidth * 0.75, and similar since they don't change.
Have you considered using
HitTestPoint instead of doing the math to check positions of containers? It's a native function, so it might be faster.
You should use a Shape instead of a Sprite if you don't need to add children to the object. e.g., your star. This might help quite a bit.
What if you created a set of star backgrounds at the start of the program. Then converted them to bitmaps, and saved them for later reuse. e.g., create a star background, convert it to bitmap data, and save this in an array. Do this, say, 10 times, and then when you need a background to just randomly select one, and apply your other shapes to it. The benefit of doing this is that you don't have to render 100-250 Sprites or Shapes each time you create a new background--that takes time.
EDIT: New idea:
Maybe you can play with the idea of only drawing the stars on the backgrounds rather than adding individual objects. The number of objects added to the screen are a big part of the problem. So I'm suggesting you draw the stars on the container directly, but with different sizes and alphas. Then scale the container down so that you get the effect you're looking for. You could reduce the display footprint from 500-1000 stars down to 0. That would be a huge improvement if you can get the effect you need from it.
Try using a single large BitmapData with it's own Bitmap, larger than the stage (although you might hit the limits of BitmapDatas if you have a really big stage and/or are using flash 9), and drawing new background images to it using the copyPixels method (a really fast way of copying pixels, faster than draw(), at least as far as I've seen).
Pan the large Bitmap around when you want and when you reach an edge, pan the bitmap to the other side, copyPixels the whole thing back to where it was previously (so the image should stay in the same place relative to the stage, but the Bitmap should move) and copyPixels new images where they are missing.
Since you are using alpha for the images as well, so you might want to check all the parameters of copyPixels if it doesn't copy alpha as you wanted it to (probably mergeAlpha?).
So, to recap, use a single large Bitmap that extends well over the boundaries of the stage, have the images ready as BitmapDatas, do the wrap trick and fill in the blanks with copyPixels from images.
I don't know if this way would perform better (the copyPixels over the whole bitmap worries me a little), but it's definitely something to try. Good luck :)