// UPDATE: Now the code is fully commented, // I have finished the remaining parts. // forked from Arena Shooting in 100 Lines // by the way it is nice to fit so much functionality into 100 lines // Hats off keim_at_Si! // What I have done to the code: // - renaming variables/functions // - explaining constructs // - moving functions outside the main constructor // What I haven't done: // - modifying the internal logic // - refactoring. (I am trying to perserve the original forms) // Arena Shooting in 100 Lines. // Movement;[Arrow key], Shot;[Ctrl], Slow & Head fix;[Shift] package { import flash.display.*; import flash.events.*; import flash.filters.*; import flash.geom.*; import flash.text.*; [SWF(width = "465", height = "465", frameRate = "30", backgroundColor = "#ffffff")] public class HundredLinesArena extends Sprite { // title text private var title_text:TextField = new TextField(); // used in enterframeEnemy private var level:int=0; //player movieclip private var player:MovieClip; //field? private var field:Sprite = stage.addChild(new Sprite()) as Sprite; //array for bullet movieclips private var bullets:Array=[]; //array for contatining shot movieclips private var shots:Array=[]; // private var ii:*; //glows are placed into an array because they will //be assigned as the filter property of movieclips private var blueglow:Array = [new GlowFilter(0x0000ff)]; // player and shot private var redglow:Array = [new GlowFilter(0xff0000)]; // enemy bullet private var score:int=0; // ever increasing frame count private var cnt:int = 0; // a bit field for keys private var keys:uint = 0; //keymap private var kmap:Array=[32,16,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,4,8]; //this vector server as the direction for the player private var unit_vector:Point = new Point(1,0); // general movieclip setter // mc: the movieclip object // props: object that will be used to set the properites of the mc // color: line color // commands: array of 1s and 2s. 1 == moveto, 2 == lineto // vertices: for every command there is a number pair in this array (a coordinate) // example: moveto lineto lineto lineto lineto // commands: [1,2,2,2,2], vertices: [-1,-1, 1,-1, 1,1, -1,1, -1,-1] // this draws a rectangle private function $(mc:*, props:*,color:uint = 0, commands:Array = null, vertices:Array = null) : void { //adding the props' object properties and values to the mc movieclip for (var p:String in props) mc[p] = props[p]; // setting the linestyle color if (color) mc.graphics.lineStyle(1, color, 1, false, "normal", null, null, 3); // uses the commands and the vertices arrays to pass them to drawPath if (commands) mc.graphics.drawPath(Vector.<int>(commands), Vector.<Number>(vertices), "nonZero"); // http://livedocs.adobe.com/flex/3/langref/flash/display/Graphics.html#drawPath() // The enterframe function of the movieclip if (props.fn) mc.addEventListener(Event.ENTER_FRAME, props.fn); // if props has an 'ar' propery (which is an array) // mc will be added to the end of this if (props.ar) mc.ar.push(mc); } //enter frame event for the player object private function enterframePlayer(e:Event) : void { // player // 00100000b = 32 dec is the bit for the shift key // when shift is not pressed r = 9, // when shift pressed r = 6 // r will be the amount of movement var r:Number = (keys & 32)?6:9; // man... I hope your collagues don't have to read your code. so... // in the keys 00000100b == 4dec is the bit for the right key // 00000001b == 1dec is the bit for the left key // 00001000b == 8dec is the bit for the down key // 00000010b == 2dec is the bit for the up key // // example: (keys & 4) is the bitwise AND operation. // also called masking. In this case 00000100b // binary field is used. This is a mask which selects // the bit for the right key. // the >> is the bit shift operator. // here you can see why shifting by two is used together with the (keys & 4) // bitmask. the (... >> 2) shifts the position 2 bit to the position 0. // after the shifting the value of ((keys&4) >> 2) is either 0 or 1 depending on the bit. var keydir:Point = new Point((( (keys & 4) >> 2) - (keys & 1)) * r, (((keys & 8) >> 3) - ((keys & 2) >> 1)) * r); // summary: keydir.x = 9 when right is pressed // keydir.x = -9 when left is pressed // keydir.y = 9 when down is pressed // keydir.y = -9 when up is pressed // we have 6 instead of 9 when the shift is hold. // if the future position player.x+keydir.x // is both bigger than -390 and less then 390 then // add the player position the difference // essentially this part is responsible for // ensuring that the player doesn't wander outside the map player.x += (player.x+keydir.x>-390 && player.x+keydir.x<390) ? keydir.x : 0; player.y += (player.y+keydir.y>-390 && player.y+keydir.y<390) ? keydir.y : 0; // to make sure the player stays in the center we have to move the field too //this would place the player to the top left corner: //field.x = (-player.x); //field.y = (-player.y); //we need the 232 offset, (approx half of the 465 witdth and height) //to place the player to the center : //field.x = (232-player.x); field.y = (232-player.y); //this is the same as: //field.x += (232-player.x)-field.x; //think of that by adding the amount on the right we //move the map closer to its ideal position // when we enclose this into a paren ()*0.1, we say that // by each frame we move 10% closer to the ideal position // that helps easing the movement. field.x += ((232-player.x) - field.x)*0.1; field.y += ((232-player.y) - field.y)*0.1; // if the shift is not pressed if (!(keys&32)) { //unit_vector stores the direction of the player // offset actually adds to the x and y coordinates // dir.x == -9, 0, 9 // dir.x * 0.05 == -0.45, 0, 0.45 // this is how the smooth turning works. // by always adding(the offset) the keydir*0.05 (-0.45,+0.45) and normalizing it, // the unit_vector will get closer to the keydir direction with each frame. // http://en.wikipedia.org/wiki/Euclidean_vector#Addition_and_subtraction // here keydir is the vector defined by the arrow keys // and the unit_vector is the actual direction of the player unit_vector.offset(keydir.x*0.05, keydir.y*0.05); unit_vector.normalize(1); // make vector length 1 // it is an interesting question why the player can do a 180 degree // turn after advancing in one direction. // the answer is simple: after the first turn the unit vector perserves // a little bit of the previous direction. This is because the fact, that by // adding to one (I) vector to another (J) many times won't make (J) point to // the exact same direction as (I). So, when you see your player advancing to // the right, the unit_vector.x is usually not exactly 1. // Ofcourse if you don't add that another direction into your unit-vector // the 180 degree turn won't be possible. This is the reason that right after // start when you press the LEFT key, the player won't do the 180 deg turn. // to investigate the issue more uncomment the following line (I have added // my wonderfl_trace to the code) then move left and right, notice that x // remains 1, now move up and down a bit and see how it works // trace("unit_vector.x: " + unit_vector.x) //rotates the player into direction player.rotation = Math.atan2(unit_vector.y, unit_vector.x)*(180/Math.PI); } // kf&16 is true when CTRL is down // cnt is increased by one on every frame // cnt&1 is true if the first bit of cnt is 1 and that happens every other frame if (!title_text.visible && (keys&16) && (cnt&1)) { // create shot var shotprops:Object= {x:player.x,y:player.y, //same pos as player v:new Point(unit_vector.x*24,unit_vector.y*24), // v for commonMoveObject, the perframe move // go same direction as player, 24 unit/frame rotation:player.rotation, // face the same direction as player r:0, // rotation per frame is none (needed by commonMoveObj) filters:blueglow, // ... yeah. right. fn:commonMoveObject, // use the commonMoveObject ar:shots}; // add every shot to the shots array //now let's draw our rectangle-buster, blue beams, this should be obvious $(field.addChild(new MovieClip()), shotprops, 0x80ffff, [1,2,1,2], [6,6,-6,6,6,-6,-6,-6]); } } private function enterframeExplosion(e:Event) : void { // explosion if ((e.target.alpha = (--e.target.cnt)*0.03) == 0) kill(e.target); else { e.target.v.x *= 0.95; // v is the velocity vector for the partice. e.target.v.y *= 0.95; // this is code for slowing the particles to 95% every frame commonMoveObject(e); } } private function commonMoveObject(e:Event) : void { // common motion // used for movieclips with both an r (rotation per frame) // and a v.x & v.y property (movement per frame) // here event.target is the movieclip which the event belongs to // the r propery is set by the $ function and it is the per frame rotation // for a particular object e.target.rotation += e.target.r; // v is a vector which has an X and an Y propery // and it defines the per frame movement for the object e.target.x += e.target.v.x; e.target.y += e.target.v.y; // objects wandering outside the x[-400;+400] y[-400;+400] territory will be killed. if (e.target.x<-400 || e.target.y<-400 || e.target.x>400 || e.target.y>400) kill(e.target); } // The distance between t and every object in the list array // will be calculated. If that distance is lower then // the sqare root of r2 (r2 == distance^2) then // t.life will be decreased // if t.life falls below 1, hit returns true private function hit(t:*, list:Array, r2:Number) : Boolean { // hit evaluation // t.l seems to be the life of the t object // // /| // http://en.wikipedia.org/wiki/Pythagorean_theorem / | // a^2 = (list[i].x-t.x)*(list[i].x-t.x) c / | b // b^2 = (list[i].y-t.y)*(list[i].y-t.y) / | // c^2 = r2 /____________| // a // condition (a^2 + b^2 < r2) // is the same as (square_root(a^2 + b^2) < sqaure_root(r2)) where // square_root(a^2 + b^2) is the distance between the 2 points // // The condition in the if is essetially responsible for calculating // the distance between the t object and each element in the list array // if the distance is less then square_root(r2) // then kill the colliding object in the list // and decrease the life of the t object // if life falls below 1, return ture for (var i:int = 0; i<list.length; i++) // iterating over the list. if ((list[i].x-t.x)*(list[i].x-t.x)+(list[i].y-t.y)*(list[i].y-t.y) < r2) { kill(list[i]); if (--t.life <= 0) return true; } return false; } private function kill(mc:*) : void { // kill object // a very ugly way to detach the movieclip from its parent and remove its // enterframe event in one step mc.parent.removeChild(mc).removeEventListener("enterFrame", mc.fn); // if the mc has an array element (which also means that it is part of that array) // then remove if (mc.ar) mc.ar = mc.ar.splice(mc.ar.indexOf(mc), 1); } private function enterframeEnemy(e:Event) : void { // enemy var i:int; e.target.cnt++; // increase the cnt propery of the enemy. cnt stores the number // of frames an enemy is staying on the scrren // (!title_text.visible): if the game is on // !(e.target.cnt % (80-level)): // and if the enemy's frame counter (e.target.cnt) is divisible by (80-level) // % is the division-remainder operator, the remainder is 0 when the stuff is divisible. // "!" inverts truthvalue // so, if level is 0 then this will be true every 80th frame // by each level the bullets are coming more and more rapidly if (!title_text.visible && !(e.target.cnt % (80-level))) { // create bullet var v:Point = new Point(player.x - e.target.x, player.y - e.target.y); // v is pointing to the player //http://livedocs.adobe.com/flex/3/langref/flash/geom/Point.html#normalize() v.normalize((Math.random()*0.06+0.03)*(50+level)); // random and level defines the length of the vector // pointing to the player // and this lenth will be used to set the per frame // advancing var bulletprops:Object = {x:e.target.x, y:e.target.y, // parent pos v:v, // previous v r:-5, // slow counterclockwise rotation fn:commonMoveObject, // give it the commonMoveObject as enterframe event ar:bullets, // place it to the bullets array filters:redglow}; //glow glow // I have detailed quite a few of these calls, check the $ function and other $ calls. $(field.addChild(new MovieClip()), bulletprops, 0xffffff, [1, 2, 2, 2, 2], [3,3,-3,3,-3,-3,3,-3,3,3]); } // check the enemy and the shots for collision. // if shot distance <= 20 then destroy, check the description of hit. if (hit(e.target, shots, 400)) { // destruction kill(e.target); // in the following line increase the score level = (++score<30) ? //if score < 30 then (score*2) : //level = score * 2 (score<60) ? //else if score < 60 then (score*0.5+45) // level = score*0.5+45 : 75; // else level = 75 // now an enemy dies. here is the part for creating the nice explosion effect for (i=0; i<8; i++) { // create particles (8) v = new Point(Math.random()*16-8, Math.random()*16-8); //each around the enemy x:+-8 y:+-8 var particleprops:Object = {x:e.target.x, y:e.target.y, //same position as enemy v:v, // same velocity vector r:20, // 20 rotation (have you noticed? they are all rotating clockwise) fn:enterframeExplosion, // give them their enterFrame function cnt:30, // this is the count used to control the fadeout effect filters:blueglow}; //.. yeah yeah. Nice glow! //if this is not trivial, check out other descriptions of the $ call. $(field.addChild(new MovieClip()), particleprops, 0x80ffff, [1, 2, 2, 2, 2], [5,5,5,-5,-5,-5,-5,5,5,5]); } } // the enemies user the commonMoveObj to rotate (r per frame) and // move towards their (v.x, v.y) direction // every frame commonMoveObject(e); } function HundredLinesArena() { WTrace.initTrace(stage); var i:int; // score board var scoreboard:TextField = new TextField(); $(title_text,{ x:180, y:230, htmlText:"<font color='#406080' size='40' face='_sans'>100Lines Arena</font>", width:282 }); addChild(scoreboard); addChild(title_text); // check out bitwise operators to understand how this works. // in short: each bit is responsible for storing a key status // later you find some more detail, but this is fairly easy to // understand if you know the operators and binary arithmetic stage.addEventListener(KeyboardEvent.KEY_DOWN, function(e:KeyboardEvent) : void { keys |= kmap[e.keyCode-16]; }); //set bit stage.addEventListener(KeyboardEvent.KEY_UP, function(e:KeyboardEvent) : void { keys &= ~kmap[e.keyCode-16]; }); //clear bit // Enter Frame Function addEventListener(Event.ENTER_FRAME, function(e:Event) : void { scoreboard.htmlText="<font color='#80c0ff' size='20' face='_sans'>Score:" + String(score) + "</font>"; if (title_text.visible && cnt>30 && keys!=0) title_text.visible = Boolean(level = score = 0); // yuck! // first, when the title_text is not visible it means that the game // is on (!title_text.visible) // then we have (title_text.visible = !(player.visible = !hit(player, bullets, 36))) // lets start from the inside // player.visible = !hit(player, bullets, 36) // hit compares the distance^2 of each bullet to the player // so, if a bullet is 6 unit or closer to the player, // then the hit returns true (otherwise hit returns false) // when hit returns true, the player.visible gets false // and then title_text.visible gets true. // and this is our second condition. // to roll it back: 1. when the game is on (title_text invisible) // 2. and the player gets a hit (hit returns ture) // 3. then hide player (!true) // 4. then show the title_text (game off) // 5. reset cnt (the global frame count) if (!title_text.visible && (title_text.visible = !(player.visible = !hit(player, bullets, 36)))) cnt = 0; // cnt is ever growing // cnt&31 masks all the bits above the 5th bit, since & is the logical AND operator // 31 dec == 11111 bin // this means that cnt&31 selects the value of the first 5 bit of cnt // !(++cnt)&31 evaulates to true when (++cnt)&31 == 0, that is all the 5 bits are zero // this happens every 32th call of (++cnt)&31, approx every sec with framerate 32 if (!((++cnt) & 31) && !title_text.visible) { // r is a random number between 0-32 var r:Number = Math.random() * 32; //The x position of the enemy depends on r //summary: // this expression // has 4 possible outcomes // for the x depending on the value of r // r[24..32[ : x = 0 // r[16..24[ : x = 8 // r[8..16[ : x = r - 8 //means x gets a random value between [0..8[ // r[0..8[ : x = r //means the same thing var x:Number = (r >= 24) ? // if r >= 24 then 0: // x = 0 (r >= 16) ? // else if r >= 16 8: // x = 8 (r >= 8) ? // else if r >=8 (r - 8) : // x = r-8 r; // else x = r // another one of these ugly bastards var y:Number = (r<8) ? // if r < 8 then 0: // y = 0 (r<16)? // else if r < 16 then 8: // y =8 (r<24)? // else if r < 24 then (r-16): // y = r - 16 (r-24); // else y = r - 24 // if you look closely you will notice that the above stuff makes // sure that one position will be on the side of the map var enemyprops:Object = {x:x*100-400, y:y*100-400, v:new Point(4-x,4-y), //direction of the enemy, // ensures it proceeds towards the inner side of the map fn:enterframeEnemy, // enemy enterframe cnt:0, // enemy cnt: number of frames he has been on the screen r:5, // enemy rotation per frame life:3, // enemy life, decrease with every hit filters:redglow}; // enemy glow $(field.addChild(new MovieClip()), enemyprops, 0xffff80, [1, 2, 2, 2, 2], [20,20, 20,-20, -20,-20, -20,20, 20,20]); } }); // creating the 8x8 field for (i=-400; i<=400; i+=100) //$(field, {rotationX:-30}, 0x80c0ff, [1,2,1,2], [-400,i,400,i,i,-400,i,400]); $(field, {rotationX: -30 }, // -30: the tilt of the map // you can add z: 500 to increse the view distance // remove or decrese the rotation by changing rotationX 0x80c0ff, // color [1,2,1,2] // 1 == moveto, 2 == lineto , [ -400, i, //moveto // 400, i, //lineto i, -400, //moveto i, 400] //lineto ); //This is the creation and the setting up of the player object //me is the variable which holds it // field is a movie clip (field) // field.addChild return the object // which gets passed to $ $( field.addChild(player = new MovieClip()), {fn:enterframePlayer, // enter frame function life:0, // life. you can use 1 too. its the same. hit checks if life <= 0 // so, the player dies after the hit anyways // I guess he used l:0 to make it harder to understand what it is ;) filters:blueglow }, // filter 0x80ffff, //color [1,2,2,2], //commands: 1 == moveto, 2 == lineto [ -9, 6, //moveto -9, -6, //lineto 9, 0, //lineto -9, 6] //lineto ); } // function HundredLinesArena (constructor) } // class } // package /////// WONDERFL TRACE ////// import flash.display.Sprite; import flash.display.Stage; import flash.text.TextField; import flash.text.TextFormat; //global trace function var trace:Function; //wtreace class class WTrace { private static var FONT:String = "Fixedsys"; private static var SIZE:Number = 12; private static var TextFields:Array = []; private static var trace_stage:Stage; public static function initTrace(stg:Stage):void { trace_stage = stg; trace = wtrace; } private static function scrollup():void { // maximum number of lines: 100 if (TextFields.length > 100) { var removeme:TextField = TextFields.shift(); trace_stage.removeChild(removeme); removeme = null; } for(var x:Number=0;x<TextFields.length;x++) { (TextFields[x] as TextField).y -= SIZE*1.2; } } public static function wtrace(... args):void { var s:String=""; var tracefield:TextField; for (var i:int;i < args.length;i++) { // imitating flash: // putting a space between the parameters if (i != 0) s+=" "; s+=args[i].toString(); } tracefield= new TextField(); tracefield.autoSize = "left"; tracefield.text = s; tracefield.y = trace_stage.stageHeight - 20; var tf:TextFormat = new TextFormat(FONT, SIZE); tracefield.setTextFormat(tf); trace_stage.addChild(tracefield); scrollup(); TextFields.push(tracefield); } } Now you too can understand how Arena Shooting in 100 Lines works