Now you too can understand how Arena Shooting in 100 Lines works
UPDATE: Now the code is fully commented,
I have finished the remaining parts.
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]
/**
* Copyright kalevionni ( http://wonderfl.net/user/kalevionni )
* MIT License ( http://www.opensource.org/licenses/mit-license.php )
* Downloaded from: http://wonderfl.net/c/xMWC
*/
// 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);
}
}