Helly everyone! I'm trying to dynamically add (and later remove) some movieclips inside of a triangle. Simple movieclip inside of a movieclip ain't working (it's a square in the end). Drawing a triangle is simple, addChild method is crystal clear too. The tough part comes after. Here's the code I'm trying to develop:
btn_toys_2.confirm.addEventListener(MouseEvent.MOUSE_UP, confirmToys);
import flash.display.Graphics;
var point1:Point = new Point(466, 65);
var point2:Point = new Point(370, 540);
var point3:Point = new Point(570, 540);
var vertices:Vector.<Number> = Vector.<Number>([point1.x, point1.y, point2.x, point2.y, point3.x, point3.y]);
var triangle:Sprite = new Sprite();
triangle.graphics.beginFill(0x00ff00, 1);
triangle.graphics.drawTriangles(vertices);
triangle.graphics.endFill();
addChild(triangle);
function confirmToys(e:MouseEvent){
var toy:MovieClip = new shar_001;
triangle.addChild(toy);
toy.x = Math.random()*30;
toy.y = Math.random()*30;
}
The "toy" movieclip is for some reason placed outside the triangle (0-30 x axis and 0-30 y axis).
The important part is to make "toys" appear within a triangle, doesnt have to be a movieclip. A way around this would be great too!
Thanks in advance!
It is happening this way because you have made your triangles anchor point at zero. You did this when you did
addChild(triangle);
That will always put the added child at (0,0). The only reason you triangle doesn't appear there is because you have added a cushion of empty pixels by making your points be greater than zero. Instead, you will use
addChild(triangle);
triangle.x = 370;
triangle.y = 65;
The point you want the top left corner of your triangle to be at is (370, 65). You should make your triangle points be (96, 0), (0, 475), (200, 475). Now the top left corner of the triangle is at (0,0) on the stage. Now set the triangle to (370, 65) after adding the triangle to the stage. Now the triangles anchor point is still the top left corner of the triangle, not the stage, so when you add the toy, it will be in reference to the point you expect.
// let the minimum x and y be zero, and adjust the others relative to that.
var point1:Point = new Point(96, 0);
var point2:Point = new Point(0, 475);
var point3:Point = new Point(200, 475);
var toyArray:Array = new Array();
var vertices:Vector.<Number> = Vector.<Number>([point1.x, point1.y, point2.x, point2.y, point3.x, point3.y]);
var triangle:Sprite = new Sprite();
triangle.graphics.beginFill(0x00ff00, 1);
triangle.graphics.drawTriangles(vertices);
triangle.graphics.endFill();
addChild(triangle);
// position anchor point on stage
triangle.x = 370;
triangle.y = 65;
function confirmToys(e:MouseEvent){
var p:Point = new Point(Math.random()*triangle.width,Math.random()*triangle.height);
if (isInsideTriangle(Point1,Point2,Point3,p))
{
var toy:MovieClip = new shar_001;
triangle.addChild(toy);
toyArray.push(toy);
toy.x = p.x;
toy.y = p.y;
}
}
private function isInsideTriangle(A:Point,B:Point,C:Point,P:Point):Boolean {
var planeAB:Number = (A.x-P.x)*(B.y-P.y)-(B.x-P.x)*(A.y-P.y);
var planeBC:Number = (B.x-P.x)*(C.y-P.y)-(C.x - P.x)*(B.y-P.y);
var planeCA:Number = (C.x-P.x)*(A.y-P.y)-(A.x - P.x)*(C.y-P.y);
return sign(planeAB)==sign(planeBC) && sign(planeBC)==sign(planeCA);
}
private function sign(n:Number):int {
return Math.abs(n)/n;
}
Removing the toys from the triangle should be pretty straight forward depending on the method you want to use. I added a toyArray that you can iterate through to remove them.
Checking if a position is within the desired boundaries and rejecting it if it's not is certainly a solution. However, this stops the program from being deterministic, because you never know how many tries it takes before a position within the boundaries is found.
Does that mean the program could run forever? Possibly yes, but this is so unlikely that it's not going to happen. Depending on how much of its bounding box a triangle fills, it will still produce quite a few misses though. Misses that have to be checked, rejected and tried again.
I'm not advising against this strategy because it might be a performance problem (and it might actually be one), but rather because it seems to miss the point: if positions in a triangle should be found, let's just do exactly that. All this trial and error and testing and rejecting is counterintuitive.
You only have one pseudo random number generator built in: Math.random().
That produces an evenly distributed random number between 0 and 1. (let's ignore whether the boundaries are possible values or not)
To create a 2D distribution, it's very easy to simply use two of those.
Now the problem with the even distribution is that it's even. To form a non-rectangular shape, transformations have to be applied.
Consider two edges of the triangle to be two vectors. A random point in the triangle is found by combining those two vectors linearly in a random way.
Obviously, with the untransformed random numbers, that would yield a diamond shaped boundary for the random points. To compensate for the fact that the vectors meet at one point and diverge in the other direction the square root is applied to one random number. The math behind that is not too complicated but ain't trivial either. I choose to omit it here. For more information ask a new question, math.se is probably a good place to do this.
Here's a full fledged example code to be used as a document class, which puts 1000 circles into a triangle boundary:
package
{
import flash.display.Shape;
import flash.display.Sprite;
import flash.geom.Point;
public class Main extends Sprite
{
public function Main()
{
var distribution:EvenDistribution2DTriangleBoundary = new EvenDistribution2DTriangleBoundary(new Point(100, 100), new Point(400, 50), new Point(250, 350));
for (var i:int = 0; i < 1000; ++i)
{
// create an object in each iteration of the loop
var circle:Shape = new Shape();
//add some graphics (this is unnecessary if you use a library symbol)
circle.graphics.beginFill(0xff0000, .6);
circle.graphics.drawCircle(0, 0, 3);
circle.graphics.endFill();
// add it to the display list
addChild(circle);
// reposition it with the help of the distribution object
distribution.positionDisplayObject(circle);
}
}
}
}
import flash.display.DisplayObject;
import flash.geom.Point;
internal class EvenDistribution2DTriangleBoundary
{
private var u:Point;
private var v:Point;
private var position:Point;
public function EvenDistribution2DTriangleBoundary(a:Point, b:Point, c:Point)
{
// consider corner "a" as the position of the triangle, this is arbitrary decision, but has to be consistent with the rest of this constructor
position = a;
// create two vectors from the corner that is the position to the other two corners respectively
u = b.subtract(a);
v = c.subtract(a);
}
public function getRandomPosition():Point
{
// random position formula with two random variables: position + (u + (v-u) * random1) * sqrt(random2)
var r1:Number = Math.random();
// the sqrt transforms the probability density function of the even distribution f(x) = 1 into a triangle g(y) = 2y
var r2:Number = Math.sqrt(Math.random());
// applying the above formula to create an evenly distributed random position within the triangle
return position.add(new Point((u.x + (v.x - u.x) * r1) * r2, (u.y + (v.y - u.y) * r1) * r2));
}
// convenience function to position a display object at a random position in the triangle
public function positionDisplayObject(object:DisplayObject):void
{
var position:Point = getRandomPosition();
object.x = position.x;
object.y = position.y;
}
}
Creating the random distribution is a class of its own. For the sake of simple testing, it's an internal class, thus the entire example is a single class that goes into a single file. Of course, in production code, this should be better organised.
Here are 4 results that I got:
it seems I need to transfer all of my frames/timeline code (and there's a lot!) into the external class
That isn't necessary although it is recommended. You should eventually only use class based code, but of course making that transition within a project isn't very practical.
In my example above, there are two classes: Main and EvenDistribution2DTriangleBoundary. You are only interested in the latter one. Main is just there to use the other class, create and display the circles, etc: it's a demo.
To use EvenDistribution2DTriangleBoundary in your project, create a new text file in the same directory as your .fla file named EvenDistribution2DTriangleBoundary.as with the following content:
package
{
import flash.display.DisplayObject;
import flash.geom.Point;
public class EvenDistribution2DTriangleBoundary
{
private var u:Point;
private var v:Point;
private var position:Point;
public function EvenDistribution2DTriangleBoundary(a:Point, b:Point, c:Point)
{
position = a;
u = b.subtract(a);
v = c.subtract(a);
}
public function getRandomPosition():Point
{
var r1:Number = Math.random();
var r2:Number = Math.sqrt(Math.random());
return position.add(new Point((u.x + (v.x - u.x) * r1) * r2, (u.y + (v.y - u.y) * r1) * r2));
}
public function positionDisplayObject(object:DisplayObject):void
{
var position:Point = getRandomPosition();
object.x = position.x;
object.y = position.y;
}
}
}
Now you can use that class like any other class in your project. For example, you can add the code from Main's constructor to your timeline and it should work:
var distribution:EvenDistribution2DTriangleBoundary = new EvenDistribution2DTriangleBoundary(new Point(100, 100), new Point(400, 50), new Point(250, 350));
for (var i:int = 0; i < 1000; ++i)
{
// create an object in each iteration of the loop
var circle:Shape = new Shape();
//add some graphics (this is unnecessary if you use a library symbol)
circle.graphics.beginFill(0xff0000, .6);
circle.graphics.drawCircle(0, 0, 3);
circle.graphics.endFill();
// add it to the display list
addChild(circle);
// reposition it with the help of the distribution object
distribution.positionDisplayObject(circle);
}
You should see a triangle of red circles similar to those I posted in the image of results above. Does that work?
Related
I'm making a Flash AS3 based game, and I'm building a custom font system. It works like this: a BitmapData class takes a PNG file from the library (FontSource), loops between every character in a given string and then gets its x, y, width and height inside the image from a function (getOffset), and then it uses copyPixels to draw the custom-font text.
Here's the code:
public function draw(str) {
var png = new FontSource(480, 32);
var offsets;
var char;
var rect;
var x;
var y;
this.lock();
for (var i = 0; i < str.length; i++) {
char = str.substr(i, 1);
offsets = getOffsets(char);
rect = new Rectangle(offsets[0], offsets[1], offsets[2], offsets[3]);
this.copyPixels(png, rect, new Point(x, y));
x += offsets[2];
}
this.unlock();
}
Now, the question here is: I'm building a typewriter effect class based on the ENTER_FRAME event; each new frame, a new character is added to a string. So, I wanted to ask which one of these approaches would do better related in a matter of performance:
1) Call draw() to redraw the whole BitmapData each frame.
2) Making an alpha mask and expand its width each frame.
3) Making separate Objects and adding them to stage each frame.
Thank you for your time.
As an alternative, you don't need to redraw the entire string. You can just draw more characters at its end. I'd implement this as follows: You give your bitmapped textfield a string it should draw per frame, once. It clears, then at each enter frame event it just adds 1 to the length of the string drawn, and draws only one single character. Just save more data in your class for this. For example:
public class BTF extends Sprite {
private var str:String;
private var source:FontSource; // set once!
private var bd:BitmapData; // the one that'll get drawn
private var lastIndex:int;
private var lastX:int; // coords to draw next symbol to
// assuming Y is always 0 - adjust per font
public function BTF(source:FontSource,width:int,height:int) {
this.source=source;
bd=new BitmapData(width,height,0x00808080,true); // check if I didn't mix up parameters
addChild(new Bitmap(bd)); // make it be drawn on your BTF
}
... // rest of class skipped
public function onEnterFrame(e:Event):void {
if (lastIndex>=str.length) return; // drawn all text already
var c:char=str.charAt(lastIndex++); // get next char to draw
var r:Rectangle=source.getOffsets(c); // you need to specify source for that - it's font that gives you bounds
bd.copyPixels(source.fontBitmapData,r,new Point(lastX,0)); // draw that char
lastX+=r.width; // move "cursor"
}
I know that you use graphics.clear to clear all the graphics but that clears the graphics from the stage, I would like to clear graphics in a specific pixel(s) or between x-y value how do I do that?
There's no way to do that with graphics. I just tried, drawing transparent shapes does not create holes, alas.
You should convert the graphics you have into Bitmap instance and work with pixels:
package
{
import flash.geom.Matrix;
import flash.geom.Rectangle;
import flash.display.Sprite;
import flash.display.Bitmap;
import flash.display.BitmapData;
import flash.display.DisplayObject;
public class Holey extends Sprite
{
public function Holey()
{
super();
// Lets create some example graphics.
graphics.beginFill(0x990000);
graphics.drawCircle(200, 200, 100);
graphics.endFill();
// Convert into raster and make 1 pixel transparent.
var aBit:Bitmap = rasterize(this);
aBit.bitmapData.setPixel32(50, 50, 0x00000000);
graphics.clear();
addChild(aBit);
}
private function rasterize(source:DisplayObject):Bitmap
{
// Obtain bounds of the graphics.
var aBounds:Rectangle = source.getBounds(source);
// Create raster of appropriate size.
var aRaster:BitmapData = new BitmapData(aBounds.width, aBounds.height, true, 0x00000000);
// Make an offset to capture all the graphics.
var aMatrix:Matrix = new Matrix;
aMatrix.translate(-aBounds.left, -aBounds.top);
aRaster.draw(source, aMatrix);
return new Bitmap(aRaster);
}
}
}
The way to do this would be with a mask. Using an alpha mask (both mask and maskee use cacheAsBitmap=true) you can draw transparent pixels onto the mask to erase parts. The basic approach would be:
Put all your graphics that you want to be effected by the mask in a common container (if you mean for everything to be cut, then they are already in a common container: the stage or root timeline.)
Draw a bitmap data object that has a transparent "hole" in the area you want to erase. For example:
// fill the stage with a solid rectangle
var maskBitmapData:BitmapData = new BitmapData(stage.stageWidth, stage.stageHeight, true, 0xff000000);
// erase part of it by drawing transparent pixels
maskBitmapData.fillRect(new Rectangle(20, 20, 200, 100), 0);
// create the mask object
var maskBitmap:Bitmap = new Bitmap(maskBitmapData);
maskBitmap.cacheAsBitmap = true; // this makes the mask an alpha mask
addChild(maskBitmap);
Set the container's .mask property. For example, to mask the entire main timeline:
root.cacheAsBitmap = true; // this makes the mask an alpha mask
root.mask = maskBitmap;
Open stack overflow to answer some questions, think for next hour about how holes are placed in cheese... :)
You could also set blendMode property of your hole object to BlendMode.ERASE in combination with cacheAsBitmap. This works similar to masks except you would be actually drawing the wholes and not the area outside them.
Here is an example:
//make cheese
var cheese:Sprite = new Sprite();
cheese.cacheAsBitmap = true;
stage.addChild(cheese);
cheese.x = cheese.y = 10;
//define holes
var holes:Shape = new Shape();
holes.blendMode = BlendMode.ERASE;
cheese.addChild(holes);
//draw cheese
var g = cheese.graphics;
g.beginFill(0xFFCC00);
g.drawRect(0,0,200,150);
//**Attack chees with mices.
g = holes.graphics;
for (var i:int = 0; i < 10; i++){
g.beginFill(0xFF00FF);
var hx = Math.random()*(cheese.width-7)+7;
var hy = Math.random()*(cheese.height-7)+7;
var s = Math.random()*15;
g.drawCircle(hx, hy, s);
g.endFill();
}
Result would be something like that:
edit:
Turns out that you don't need to use cacheAsBitmap if you set blend mode of parent object to LAYER (doc says it should be set automatically...)
So you can use cheese.blendMode = BlendMode.LAYER; instead of cheese.cacheAsBitmap = true;. And if I remember correctly, masks also don't require cahceAsBitmap, even with NORMAL blending mode.
I have my main stage as 550x400. The header area is a stats bar. So I have an element underneath it which I named gameStage which is 550x350.
I am creating circles on a 1 second interval and then trying to randomly place them within my gameStage. It does not appear to be working. It seems like they're being added to a 550x350 element, but it starts at the top of my main stage -- not within my gameStage.
Also if I simply do addChild(circle) it creates an even 25 radius circle. As soon as I do gameStage.addChild(circle), the circle gets skewed slightly.
What am I doing wrong?
private function createCircle():void {
var stageSafeX:Number = Math.random()*gameStage.width;
var stageSafeY:Number = Math.random()*gameStage.height;
var circle:Sprite = new Sprite();
circle.graphics.clear();
circle.graphics.beginFill(Math.random()*0xFFFFFF, 1);
circle.graphics.drawCircle(0, 0, circleRadius);
circle.graphics.endFill();
circle.x = stageSafeX;
circle.y = stageSafeY;
circle.name = String(circleCount);
gameStage.addChild(circle);
}
Okay I'm using Flash Develop, so you'll have to forgive me as this program doesn't have FLA files, only classes and it uses a Main class to start the program (more reminiscent of Java if you've ever programmed in that). But the code I'll show you is more or less the same of how you want to do it.
First I would recommend you make a randomNumber function, I used it in making this code so if you want to use it here's the one I use (I put this in the Main class, you can put this wherever you want):
public static function randomNumber(minValue:Number, maxValue:Number):uint {
return Math.floor(Math.random() * (1 + maxValue - minValue)) + minValue;
}
This is inclusive, meaning if you put randomNumber(1, 10) it will give you a number between 1 to 10, including 1 and 10. It's more or less common sense, but I figured I might as well mention it just to clarify.
Now on to the addCircle function:
public static function addCircle(gameStage:Sprite, circleRadius:uint):void {
//Initializing the new circle instance
var newCircle:Sprite = new Sprite();
//Basically the same code you had (you don't need to set the alpha value to 1, it's default value is 1 regardless)
newCircle.graphics.beginFill(Math.random() * 0xFFFFFF);
newCircle.graphics.drawCircle(0, 0, circleRadius);
newCircle.graphics.endFill();
//Since the circle's origin is the center, you want its outer edges to be bound to the gameStage's edges
var safeStageX:Number = Main.randomNumber(newCircle.width / 2, gameStage.width - newCircle.width / 2);
var safeStageY:Number = Main.randomNumber(newCircle.height / 2, gameStage.height - newCircle.height / 2);
//Adding the circle to the gameStage's display field
gameStage.addChild(newCircle);
//Only set the circle's x and y AFTER you add it to the gameStage's display list, otherwise it might not set properly
newCircle.x = safeStageX;
newCircle.y = safeStageY;
}
Now following up I will give the code I made for the creation of the gameStage. You probably already have something for it, but I'll provide mine just in case you want to use it instead:
//Initializing the gameStage instance
var gameStage:Sprite = new Sprite();
//Adding the gameStage to the Stage's display field
this.stage.addChild(gameStage);
//Setting the gameStage's width and height (using "gameStage.width = 550" and "gameStage.height = 350" WILL NOT WORK)
//Use the color of your main game's background so you don't see this fill (unless you want to)
//Either do this or add a background picture, you need to do one or the other in order to set the gameStage's dimensions
gameStage.graphics.beginFill(0x000000);
gameStage.graphics.drawRect(0, 0, 550, 350);
gameStage.graphics.endFill();
//This puts the gameStage on the bottom of the screen (since it's 50 pixels shorter in the y direction)
gameStage.y = 50;
Lastly I will give you the actual for loop to create your circles (this function is present in the same class/FLA that your gameStage is on, because the addCircle function needs to take in that gameStage instance:
//Now let's populate your gameStage
for (var i:uint = 0; i < [number of circles you want]; i++) {
Main.addCircle(gameStage, [radius of the circle]);
}
And you're done! I'll also include the entire Main class, just so you can see how all the functions work together.
package {
import flash.display.Shape;
import flash.display.Sprite;
import flash.events.Event;
public class Main extends Sprite {
public function Main() {
if (stage) init();
else addEventListener(Event.ADDED_TO_STAGE, init);
}
private function init(e:Event = null):void {
removeEventListener(Event.ADDED_TO_STAGE, init);
var gameStage:Sprite = new Sprite();
this.stage.addChild(gameStage);
gameStage.graphics.beginFill(0x000000);
gameStage.graphics.drawRect(0, 0, 550, 350);
gameStage.graphics.endFill();
gameStage.y = 50;
for (var i:uint = 0; i < 150; i++) {
Main.addCircle(gameStage, Main.randomNumber(15, 25));
}
}
public static function addCircle(gameStage:Sprite, circleRadius:uint):void {
var newCircle:Sprite = new Sprite();
newCircle.graphics.beginFill(Math.random() * 0xFFFFFF);
newCircle.graphics.drawCircle(0, 0, circleRadius);
newCircle.graphics.endFill();
var safeStageX:Number = Main.randomNumber(newCircle.width / 2, gameStage.width - newCircle.width / 2);
var safeStageY:Number = Main.randomNumber(newCircle.height / 2, gameStage.height - newCircle.height / 2);
gameStage.addChild(newCircle);
newCircle.x = safeStageX;
newCircle.y = safeStageY;
}
public static function randomNumber(minValue:Number, maxValue:Number):uint {
return Math.floor(Math.random() * (1 + maxValue - minValue)) + minValue;
}
}
}
i'm having problems with my code or something... the thing is i'm using lineTo from one objects coordinates to the other's, but no mater where the second object is the line always goes off to a random direction somewhere in the lower left corner and i'm stuck.
here is the code:
var spr:Shape = new Shape();
spr.graphics.clear();
spr.graphics.lineStyle(2,0xffffff);
spr.x = latM[1].x;
spr.y = latM[1].y;
spr.graphics.lineTo(latM[0].x,latM[0].y);
trace("latM[0].x = "+latM[0].x+"\tlatM[0].y = "+latM[0].y+
"\nlatM[1].x = "+latM[1].x+"\tlatM[1].y = "+latM[1].y);
spr.graphics.lineTo(latM[0].x,latM[0].y);
addChild(spr);
after a few tries i found out that all lines point [wrote lean by mistake] towards the lower left TT_TT..
I assume latM[1] and latM[0] are the two shapes your trying to draw a line between. If that is case did you notice you have two lineTo going to the same point?
What you need is.
spr.graphics.moveTo(latM[0].x, latM[0].y);
spr.graphics.lineTo(latM[1].x, latM[1].y);
Here is a small prototype to show you how it works. (This is not meant to be super solid code it is a quick and dirty prototype.)
package src
{
import flash.display.Sprite;
import flash.events.Event;
public class Main extends Sprite
{
private var obj1:Sprite = new Sprite();
private var obj2:Sprite = new Sprite();
private var lineSprite:Sprite = new Sprite();
// for testing your line.
// we don't really need it for this prototype however it
// is being used since this is how your accessing your Objects.
private var latM:Array = [];
public function Main()
{
addEventListener(Event.ADDED_TO_STAGE, initMain);
}
private function initMain(e:Event):void
{
removeEventListener(Event.ADDED_TO_STAGE, initMain);
obj1.graphics.lineStyle(1, 0);
obj1.graphics.beginFill(0xccccff);
obj1.graphics.drawCircle(0, 0, 20);
obj1.graphics.endFill();
obj1.x = 100;
obj1.y = 100;
obj2.graphics.lineStyle(1, 0);
obj2.graphics.beginFill(0xffcccc);
obj2.graphics.drawCircle(0, 0, 20);
obj2.graphics.endFill();
obj2.x = 400;
obj2.y = 200;
// for testing your line.
latM.push(obj1, obj2);
addChild(obj1);
addChild(obj2);
addChild(lineSprite);
addEventListener(Event.ENTER_FRAME, handleEnterFrame);
}
private function handleEnterFrame(e:Event):void
{
// this will clear and redraw the line between the two sprites
// every frame and thus always be up to date.
lineSprite.graphics.clear();
lineSprite.graphics.lineStyle(2, 0xff0000);
lineSprite.graphics.moveTo(latM[0].x, latM[0].y);
lineSprite.graphics.lineTo(latM[1].x, latM[1].y);
//obj1.x++; // uncomment this line and you can watch it move and keep the line perfect.
}
}
}
What do you mean lean towards the lower left?
You can only draw a straight line with lineTo.
"lineTo" only goes from the current point to the point set via its parameters.
The moveTo function will move the point without drawing.
The following code will draw a box 100 X 100
var spr:Shape = new Shape();
spr.graphics.clear();
spr.graphics.lineStyle(2,0xff00ff);
spr.graphics.moveTo(0,0);
spr.graphics.lineTo(0,100);
spr.graphics.lineTo(100,100);
spr.graphics.lineTo(100,0);
spr.graphics.lineTo(0,0);
addChild(spr);
I am making a Sim City like game. There are lots of tiles. When I first started. I was just using a tilesheet. I was copying the necessary pieaces from the tilesheet. on to a blank bitMapData. I then took the bitMapData and put it into a bitMap which I then put into a DisplayObject. It worked great!
tileSheet:BitMapData <----- data is already in
loop { loop through and tiled
bg:bitMapData= new bitMapData();
bg.copyPixel(tileSheet,rect,point);
}
canvas.BitMap(bg);
addChild(canvas);
Only problem was I needed to make my tiles interactive. I needed to highlight them and change colors and stuff. So I used the Sprite object. It works great but I can only have so many on the stage at once. or else it moves slow when I scroll. I need something Lighter then a sprite, but yet I can still turn into a object to make interactive. Anyone have any ideas ???
If you have a lot of tiles, that will impact performance because Flash needs to update the transformations of a lot of display objects (which internally means a lot of matrix calculations, and subsequent redraws of big areas of the screen.)
There is another way to achieve interactivity, if you find that you must use a single bitmap data for performance. Keep an "abstract" (i.e. not graphical) data model in memory, that stores your game state. Make sure that you are able to read from your store where a certain element is positioned in the game world. Then you can use a flat bitmap data to render the game world, because the individual positions are stored elsewhere.
When the user clicks the DisplayObject containing the bitmap data (a Sprite in which the bitmap is drawn using a bitmap fill, or that wraps a Bitmap), look in your model which of your game elements was hit by that click.
// myTileSprite is a Sprite with a bitmap fill
myTileSprite.addEventListener(MouseEvent.CLICK, handleWorldClick);
function handleWorldClick(ev : MouseEvent) : void
{
var i : int;
// Loop through all game element data models
for (i=0; i<myGameElements.length; i++) {
// Test the mouse position against the element model
if (myGameElements[i].hitTest(myTileSprite.mouseX, myTileSprite.mouseY)) {
trace('this was the element that was clicked: '+myGameElements[i].toString());
}
}
}
Here, whenever the player clicks the world graphics, the loop tries to find that element which was directly under the mouse position. You will need to implement a hitTest() method on all your game element data models, of course. Such a method simply checks the supplied world space position against the tile's area:
// GameElement.hitTest():
/**
* Tests a world position against the position and area of this game
* element tile. Returns a boolean indicating whether this tile was hit.
*/
public function hitTest(mouseX : Number, mouseY : Number) : void
{
var rect : Rectangle = new Rectangle(this.worldX, this.worldY, this.width, this.height);
if (mouseX > rect.left && mouseX < rect.right
&& mouseY > rect.top && mouseY < rect.top) {
return true;
}
else return false;
}
The GameElement class is not an display object, but has worldX and worldY properties indicating where it is located in the world. It's width and height properties define it's dimensions.
The trick from hereon is to make sure that the rendered bitmap and your model storage is synchronized, so that a tile's position on the bitmap really corresponds to it's worldX/worldY properties in the data model.
I am one step ahead of you. And that is a great idea. Its alot easier to keep a data representation of the world when the tiles are squared. I therefore can take my mouseX/tileWidth, and thats hw many columns I moved from left to right. same with the Y axis.
Not only that but coordinates start at top left corner.
But issue I have is that my tiles are Isometric. So instead of the X axis start off like...
012345678
0
1
2
3
4
5
6
7
8
My tiles are aligned like...
00
1 1
2 2
3 3
4 4
5 6
its a little sloppy. but the right side represents the y axis and the left represents the x axis. and the center origin is in the center of the screen. not on the top left. I am trying to figure out how to measure where my mouse is from the center and out on both sides. This sounds extremely difficult. I am not sure if its possible. The game is suppose to be like a sim city like game. The first sim city was squares not isometric. I dont think they went isometric until they started using 3d. I wonder if its possible to create a illusion of isometric on a square tile.
Ive been reading this great book on isometrics. They show to calculate tiles in 3d space. and even calculate your mouse in 3d space as well. here is the code. Its alot, but I hope someone else understands it more then I. The book was written by jobe makar on building multiplayer worlds. I wanted to share it because the code it is pretty simple as far as amount of code put into it. only 2 classes needed. I am not that good with trigonometry. so I cant really interpret how the math is getting the results. hopefully someone can explain that for me :D.
Y coordinates are not given because the width is = to height. The coordinates method is just a custom made Point class which holds x, y and z.
package com.gamebook.grid {
import com.gamebook.utils.geom.Coordinate;
import com.gamebook.utils.Isometric;
import flash.display.MovieClip;
import flash.events.MouseEvent;
/**
* ...
* #author Jobe Makar - jobe#electrotank.com
*/
public class Map extends MovieClip{
private var _grid:Array;
private var _iso:Isometric;
private var _tileWidthOnScreen:int;
private var _tileHeightOnScreen:int;
private var _tileWidth:Number;
private var _tileHeight:Number;
private var _cols:int;
private var _rows:int;
private var _lastTile:Tile;
public function Map() {
initialize();
}
private function initialize():void{
_iso = new Isometric();
//when mapped to the screen the tile makes a diamond of these dimensions
_tileWidthOnScreen = 64;
_tileHeightOnScreen = 32;
//figure out the width of the tile in 3D space
_tileWidth = _iso.mapToIsoWorld(64, 0).x;
//the tile is a square in 3D space so the height matches the width
_tileHeight = _tileWidth;
buildGrid();
addEventListener(MouseEvent.MOUSE_MOVE, mouseMoved);
}
private function mouseMoved(e:MouseEvent):void {
if (_lastTile != null) {
_lastTile.alpha = 1;
_lastTile = null;
}
var coord:Coordinate = _iso.mapToIsoWorld(mouseX, mouseY);
var col:int = Math.floor(coord.x / _tileWidth);
var row:int = Math.floor(Math.abs(coord.z / _tileHeight));
if (col < _cols && row < _rows) {
var tile:Tile = getTile(col, row);
tile.alpha = .5;
_lastTile = tile;
}
}
private function buildGrid():void{
_grid = [];
_cols = 10;
_rows = 10;
for (var i:int = 0; i < _cols;++i) {
_grid[i] = [];
for (var j:int = 0; j < _rows;++j) {
var t:Tile = new Tile();
var tx:Number = i * _tileWidth;
var tz:Number = -j * _tileHeight;
var coord:Coordinate = _iso.mapToScreen(tx, 0, tz);
t.x = coord.x;
t.y = coord.y;
_grid[i][j] = t;
addChild(t);
}
}
}
private function getTile(col:int, row:int):Tile {
return _grid[col][row];
}
}
}
Then we have the isometric class that calculates 3d space.
package com.gamebook.utils {
import com.gamebook.utils.geom.Coordinate;
/**
* #author Jobe Makar - jobe#electrotank.com
*/
public class Isometric {
//trigonometric values stored for later use
private var _sinTheta:Number;
private var _cosTheta:Number;
private var _sinAlpha:Number;
private var _cosAlpha:Number;
/**
* Isometric class contrustor.
* #param declination value. Defaults to the most common value, which is 30.
*/
public function Isometric() {
var theta:Number = 30;//even though the tiles are already isometric, you still have to put the degrees the tiles will be turned.
var alpha:Number = 45;//45 degrees on y axis, 30 dgrees on x axis
theta *= Math.PI/180; // then you translate to radians
alpha *= Math.PI/180;
_sinTheta = Math.sin(theta);
_cosTheta = Math.cos(theta);
_sinAlpha = Math.sin(alpha);
_cosAlpha = Math.cos(alpha);
}
/**
* Maps 3D coordinates to the 2D screen
* #param x coordinate
* #param y coordinate
* #param z coordinate
* #return Coordinate instance containig screen x and screen y
*/
public function mapToScreen(xpp:Number, ypp:Number, zpp:Number):Coordinate {
var yp:Number = ypp;
var xp:Number = xpp*_cosAlpha+zpp*_sinAlpha;
var zp:Number = zpp*_cosAlpha-xpp*_sinAlpha;
var x:Number = xp;
var y:Number = yp*_cosTheta-zp*_sinTheta;
return new Coordinate(x, y, 0);
}
/**
* Maps 2D screen coordinates into 3D coordinates. It is assumed that the target 3D y coordinate is 0.
* #param screen x coordinate
* #param screen y coordinate
* #return Coordinate instance containig 3D x, y, and z
*/
public function mapToIsoWorld(screenX:Number, screenY:Number):Coordinate {
var z:Number = (screenX/_cosAlpha-screenY/(_sinAlpha*_sinTheta))*(1/(_cosAlpha/_sinAlpha+_sinAlpha/_cosAlpha));
var x:Number = (1/_cosAlpha)*(screenX-z*_sinAlpha);
return new Coordinate(x, 0, z);
}
}
}