From: dsc Date: Tue, 23 Nov 2010 04:52:05 +0000 (-0800) Subject: Fixes bullet projection pathfinding to only consider future paths on the current... X-Git-Url: http://git.less.ly:3516/?a=commitdiff_plain;h=e379fc14aa29706a0de286d1470ebc8d51cf306f;p=tanks.git Fixes bullet projection pathfinding to only consider future paths on the current trajectory. --- diff --git a/src/Y/y-collection.js b/src/Y/y-collection.js index 5cc6068..9e7ee7b 100644 --- a/src/Y/y-collection.js +++ b/src/Y/y-collection.js @@ -116,7 +116,7 @@ YBase.subclass('YCollection', { var args = Y(arguments), name = args.shift(); return this.map(function(o){ - return o[name].apply(o, args); + return o && o[name].apply(o, args); }); } diff --git a/src/lessly/log.js b/src/lessly/log.js index b1d85bb..3fa7703 100644 --- a/src/lessly/log.js +++ b/src/lessly/log.js @@ -8,7 +8,8 @@ Log = new Y.Class('Log', { init : function init(el, options){ this.el = el; var o = Y({}, this.DEFAULT_OPTIONS, options || {}); - this.options = o.end(); + // this.options = o.end(); + this.options = o; this.prefix = (this.options.prefix ? '['+prefix+'] ' : ''); this.id = Log._n++; diff --git a/src/portal/math/line.js b/src/portal/math/line.js index e6b5172..11734b8 100644 --- a/src/portal/math/line.js +++ b/src/portal/math/line.js @@ -59,6 +59,11 @@ math.Line = new Y.Class('Line', math.Vec, { this.y1 + t*this.pb ); }, + iparametric : function iparametric(x,y){ + return new math.Vec( (x-this.x1)/this.pa , + (y-this.y1)/this.pa ); + }, + pointAtX : function pointAtX(x){ return new math.Vec(x, this.calcY(x)); }, pointAtY : function pointAtY(y){ return new math.Vec(this.calcX(y), y); }, calcY : function calcY(x){ return (x === this.xint ? 0 : x*this.slope + this.yint); }, diff --git a/src/portal/math/vec.js b/src/portal/math/vec.js index 4154495..7d5a2e0 100644 --- a/src/portal/math/vec.js +++ b/src/portal/math/vec.js @@ -64,6 +64,15 @@ math.Vec = new Y.Class('Vec', [], { return this.x*b.x + this.y*b.y; }, + manhattan: function manhattan(x2,y2) { + if (x2 instanceof math.Vec) { + y2 = x2.y; x2 = x2.x; + } + var d1 = Math.abs(x2 - this.x) + , d2 = Math.abs(y2 - this.y) ; + return d1 + d2; + }, + rotate : function rotate(theta){ var sin = Math.sin(theta) , cos = Math.cos(theta) @@ -96,6 +105,29 @@ Y.extend(math.Vec, { lerp : function lerp(x, a, b) { return new math.Vec( math.lerp(a.x, b.x, x), math.lerp(a.y, b.y, x) ); - } + }, + + // manhattan: function manhattan(p1, p2) { + // var d1 = Math.abs(p2.x - p1.x) + // , d2 = Math.abs(p2.y - p1.y) ; + // return d1 + d2; + // }, + + // manhattan4: function manhattan4(x1,y1, x2,y2) { + // var d1 = Math.abs(x2 - x1) + // , d2 = Math.abs(y2 - y1) ; + // return d1 + d2; + // }, + + manhattan: function manhattan(x1,y1, x2,y2) { + if (x1 instanceof math.Vec && y1 instanceof math.Vec) { + y2 = y1.y; x2 = y1.x; + y1 = x1.y; x1 = x1.x; + } + var d1 = Math.abs(x2 - x1) + , d2 = Math.abs(y2 - y1) ; + return d1 + d2; + }, + }); diff --git a/src/portal/util/tree/quadtree.js b/src/portal/util/tree/quadtree.js index b7a5afb..8b23c47 100644 --- a/src/portal/util/tree/quadtree.js +++ b/src/portal/util/tree/quadtree.js @@ -69,6 +69,7 @@ QuadTree = new Y.Class('QuadTree', { }, + // TODO: Add optional filter get : function get(x1,y1, x2,y2){ return this.collect(x1,y1, x2,y2, this._get, { vals:Y([]) }, this).vals; }, @@ -81,6 +82,9 @@ QuadTree = new Y.Class('QuadTree', { return acc; }, + // XXX: Since this is by far the most commonly called function, it'd be good to + // collapse the recursion and reduce the number of function-calls. But it's not a + // bottleneck yet, so we'll wait. // _get : function _get(_x1,_y1, _x2,_y2, values){ // var self = this // , cxt = context || self diff --git a/src/tanks/map/level.js b/src/tanks/map/level.js index 413110b..8909eff 100644 --- a/src/tanks/map/level.js +++ b/src/tanks/map/level.js @@ -3,11 +3,11 @@ Level = Rect.subclass('Level', { init : function init(game, w,h){ Rect.init.call(this, w,h); + this.canvas.remove(); + this.game = game; - this.pathmap = new PathMap(this, 0,0, w, h, CAPACITY); this.walls = []; - - this.canvas.remove(); + this.pathmap = new PathMap(this, 0,0, w, h, CAPACITY); game.addEventListener('ready', this.setup.bind(this)); }, @@ -20,7 +20,7 @@ Level = Rect.subclass('Level', { [1,7, 2,2], [3,6, 1,1], [2,2, 1,1] ], - "this.addWall.apply(this, Y.map(_, '*50'))", + "this.addWall.apply(this, Y.map(_, '*REF_SIZE'))", this ); P = @@ -32,12 +32,13 @@ Level = Rect.subclass('Level', { game.addUnit(new Tank(2), 8,1); }, - addWall : function addWall(x,y, w,h){ - var wall = new Level.Wall(x,y, w,h); + addWall : function addWall(x,y, w,h, isBoundary){ + var wall = new Level.Wall(x,y, w,h, isBoundary); this.walls.push(wall); this.pathmap.addBlocker(wall); - return this.append( wall.render(this).shape ); + this.append( wall.render(this).shape ); + return wall; }, drawShape : Y.op.nop @@ -45,6 +46,7 @@ Level = Rect.subclass('Level', { }); Level.Wall = Thing.subclass('Wall', { + isBoundary : false, blocking : true, active : false, @@ -57,9 +59,10 @@ Level.Wall = Thing.subclass('Wall', { }, - init : function initWall(x,y, w,h){ + init : function initWall(x,y, w,h, isBoundary){ this.width = w; this.height = h; + this.isBoundary = !!isBoundary; Thing.init.call(this); this.setLocation(x,y); }, @@ -72,7 +75,11 @@ Level.Wall = Thing.subclass('Wall', { render : function render(parent){ - if (this.shape) this.shape.remove(); + if (this.isBoundary) + return this; + + if (this.shape) + this.shape.remove(); this.shape = new Rect(this.width, this.height) diff --git a/src/tanks/map/pathmap.js b/src/tanks/map/pathmap.js index b57fe9b..51be52e 100644 --- a/src/tanks/map/pathmap.js +++ b/src/tanks/map/pathmap.js @@ -1,7 +1,7 @@ // -*- mode: JavaScript; tab-width: 4; indent-tabs-mode: nil; -*- (function(){ -PathMap = new Y.Class('PathMap', QuadTree, { +PathMap = QuadTree.subclass('PathMap', { gridSquareSize : REF_SIZE, gridSquareMidPt : new math.Vec(REF_SIZE/2, REF_SIZE/2), @@ -11,19 +11,23 @@ PathMap = new Y.Class('PathMap', QuadTree, { x1 -= 1; y1 -= 1; x2 += 1; y2 += 1; QuadTree.init.call(this, x1,y1, x2,y2, capacity); this.level = level; - this.game = level.game; - - var w = this.width, h = this.height; - this.walls = { - top : new Level.Wall(x1,y1, w,1), - bottom : new Level.Wall(x1,y2-1, w,1), - left : new Level.Wall(x1,y1, 1,h), - right : new Level.Wall(x2-1,y1, 1,h) + this.game = level.game; + this.walls = level.walls; + + this.game.addEventListener('ready', this.setup.bind(this, x1,y1, x2,y2)); + }, + + setup : function setup(x1,y1, x2,y2){ + var w = this.width, h = this.height, level = this.level + + , BWs = { + top : [x1,y1, w,1, true], + bottom : [x1,y2-1, w,1, true], + left : [x1,y1, 1,h, true], + right : [x2-1,y1, 1,h, true] }; - Y(this.walls).forEach(function(wall){ - wall.isBoundary = true; - this.addBlocker(wall); - }, this); + + this.boundaryWalls = Y.map(BWs, 'this.level.addWall.apply(this.level, _)', this); }, addBlocker : function addBlocker(obj){ @@ -101,7 +105,10 @@ PathMap = new Y.Class('PathMap', QuadTree, { }, - + /** + * @protected Creates a pathing grid suitable for A* search. + * TODO: Fold A* into this class. + */ grid : function grid(){ if ( !this._grid ) { var size = this.gridSquareSize @@ -170,18 +177,6 @@ PathMap = new Y.Class('PathMap', QuadTree, { return new math.Vec(floor(x)*size, floor(y)*size); }, - manhattan: function manhattan(p1, p2) { - var d1 = Math.abs(p2.x - p1.x) - , d2 = Math.abs(p2.y - p1.y) ; - return d1 + d2; - }, - - manhattan4: function manhattan4(x1,y1, x2,y2) { - var d1 = Math.abs(x2 - x1) - , d2 = Math.abs(y2 - y1) ; - return d1 + d2; - }, - path : function path(start, end, id){ var size = this.gridSquareSize, floor = Math.floor , grid = this.grid() @@ -263,7 +258,7 @@ PathMap = new Y.Class('PathMap', QuadTree, { ctx.fill(); ctx.closePath(); } - this.drawStep = drawStep; + // this.drawStep = drawStep; ctx.fillStyle = 'rgba(0,0,0,0.1)'; drawStep( start ); @@ -301,9 +296,10 @@ PathMap = new Y.Class('PathMap', QuadTree, { overlay : function overlay(gridEl){ var w = this.width-2 , h = this.height-2 - , canvas = $('.overlay', gridEl)[0]; + , canvas = $('.overlay', gridEl)[0] + ; - if (!canvas) { + if ( !canvas ) { canvas = $('').prependTo(gridEl)[0]; $(canvas).width(w).height(h); canvas.width = w; diff --git a/src/tanks/map/trajectory.js b/src/tanks/map/trajectory.js index 344fe1b..6b35eea 100644 --- a/src/tanks/map/trajectory.js +++ b/src/tanks/map/trajectory.js @@ -1,4 +1,4 @@ -Trajectory = new Y.Class('Trajectory', math.Line, { +Trajectory = math.Line.subclass('Trajectory', { halt : false, elapsed : 0, bounces : 0, @@ -19,7 +19,8 @@ Trajectory = new Y.Class('Trajectory', math.Line, { var pm = this.pathmap , ex = (x1 > x2 ? -REF_SIZE : REF_SIZE+pm.width ) , ey = (y1 > y2 ? -REF_SIZE : REF_SIZE+pm.height) - , edge = this.near(ex,ey); + , edge = this.near(ex,ey) + ; // Move goal point beyond far wall to avoid rotations x2 = this.x2 = edge.x; @@ -33,6 +34,7 @@ Trajectory = new Y.Class('Trajectory', math.Line, { }, // Determine how much time can pass before we risk teleporting + // We'll need to reset this whenever the bounding box changes size resetBound : function resetBound(){ var BOUND_SIZE_RATIO = 0.75 , abs = Math.abs @@ -57,6 +59,7 @@ Trajectory = new Y.Class('Trajectory', math.Line, { to = this.stepTo(t, bb); if (this.halt) break; + // FIXME: bb.add? bb = new Loc.Rect(to.x,to.y, to.x+bw,to.y+bh); } while (dt > 0); @@ -98,18 +101,18 @@ Trajectory = new Y.Class('Trajectory', math.Line, { this.reset(to.x,to.y, ng.x,ng.y); owner.render(this.game.level); - false && console.log([ - '['+TICKS+' ('+this.depth+')] '+owner+' reflected!', - ' wanted: '+_to+' x ('+(_to.x+bb.width)+','+(_to.y+bb.height)+')', - ' blocker: '+test.msg, - ' old:', - ' loc: '+bb.p1, - ' goal: '+og, - ' new:', - ' loc: '+to, - ' goal: '+ng, - ' --> trajectory: '+this - ].join('\n')); + // console.log([ + // '['+TICKS+' ('+this.depth+')] '+owner+' reflected!', + // ' wanted: '+_to+' x ('+(_to.x+bb.width)+','+(_to.y+bb.height)+')', + // ' blocker: '+test.msg, + // ' old:', + // ' loc: '+bb.p1, + // ' goal: '+og, + // ' new:', + // ' loc: '+to, + // ' goal: '+ng, + // ' --> trajectory: '+this + // ].join('\n')); } return to; diff --git a/src/tanks/thing/bullet.js b/src/tanks/thing/bullet.js index 4e19d45..1d3dc9b 100644 --- a/src/tanks/thing/bullet.js +++ b/src/tanks/thing/bullet.js @@ -8,17 +8,18 @@ Bullet = Thing.subclass('Bullet', { * @param {math.Line} trajectory */ init : function initBullet(owner, x2,y2){ - var self = this; this.owner = owner; - this.game = owner.game; + this.game = owner.game; + Thing.init.call(this, owner.align); var loc = owner.getTurretLoc() , x1 = loc.x, y1 = loc.y; + this.setLocation(x1,y1); this.trajectory = new Trajectory(this, x1,y1, x2,y2, this.stats.move*REF_SIZE/1000); - this.addEventListener('collide', self.onCollide.bind(this)); + this.addEventListener('collide', this.onCollide.bind(this)); }, @@ -28,8 +29,8 @@ Bullet = Thing.subclass('Bullet', { offsetX : -3, offsetY : -3, - width : 6, - height : 6, + width : 6, + height : 6, stats : { move : 2.0 // move speed (squares/sec) diff --git a/src/tanks/thing/tank.js b/src/tanks/thing/tank.js index 28034db..1948e5f 100644 --- a/src/tanks/thing/tank.js +++ b/src/tanks/thing/tank.js @@ -30,6 +30,7 @@ Tank = Thing.subclass('Tank', { currentMoveLimit : -1, + init : function init(align){ Thing.init.call(this, align); this.onBulletDeath = this.onBulletDeath.bind(this); @@ -40,36 +41,14 @@ Tank = Thing.subclass('Tank', { }); }, - ableToShoot : function ableToShoot(){ - return this.nShots < this.stats.shots && this.cooldowns.attack.ready; - }, + onBulletDeath : function onBulletDeath(evt){ this.nShots--; }, + - findClosest : function findClosest(agents){ - if (!agents || !agents.size()) - return null; - - var manhattan = this.game.pathmap.manhattan - , bb = this.boundingBox, mid = this.midpoint - - agents.sort(function(a,b){ - return Y.op.cmp( - manhattan(a.loc,mid), - manhattan(b.loc,mid) ); - }); - - return agents.attr(0); - }, - willCollide : function willCollide(bullets, wiggle){ - wiggle = wiggle || 0; - var bb = this.boundingBox, mid = this.midpoint - , w = (bb.width+wiggle)/2, h = (bb.height+wiggle)/2 - ; - return bullets.filter(function(b){ return !b.dead && b.trajectory.isWithin(mid, w,h); }); - }, act : function act(){ + // Check to see if we should obey our last decision, and not recalc if ( this.forceCurrentMove && this.forceCurrentMove() && this.currentMoveLimit > NOW ) { this.continueMove(); return this; @@ -83,7 +62,7 @@ Tank = Thing.subclass('Tank', { // Try to shoot down nearby bullets var bs = this.willCollide( this.findNearLike(16, Y.is(Bullet)) ); if ( bs.size() ) { - var b = this.findClosest(bs); + var b = this.closestOf(bs); // console.log('Incoming! Shoot it down!', b); this.shoot(b.loc.x, b.loc.y); @@ -93,7 +72,7 @@ Tank = Thing.subclass('Tank', { // Dodge incoming bullet var bs = this.willCollide( this.findNearLike(50, Y.is(Bullet)), 10 ); if ( bs.size() ) { - var b = this.findClosest(bs), bs = [b] + var b = this.closestOf(bs), bs = [b] , mid = this.midpoint , trj = b.trajectory.tangent(mid) @@ -102,7 +81,7 @@ Tank = Thing.subclass('Tank', { , y = (mid.y > h/2 ? h : 0) , to = this.currentMove = trj.near(x,y); - this.forceCurrentMove = function(){ return this.willCollide(bs, 10); }; + this.forceCurrentMove = this.willCollide.bind(this, bs, 10); this.currentMoveLimit = NOW + 750; this.move(to.x, to.y); @@ -110,7 +89,7 @@ Tank = Thing.subclass('Tank', { } // Try to blow up nearby tanks - var t = this.findNearLike(66, 'Y.is(Tank, _) && _.align !== this.align').shift(); + var t = this.findNearEnemies(66).shift(); if (t) { // console.log('I gotcha!', t); this.shoot(t.loc.x, t.loc.y); @@ -125,9 +104,39 @@ Tank = Thing.subclass('Tank', { return this; }, + ableToShoot : function ableToShoot(){ + return this.nShots < this.stats.shots && this.cooldowns.attack.ready; + }, + + closestOf : function closestOf(agents){ + if (!agents || !agents.size()) + return null; + + var manhattan = math.Vec.manhattan + , bb = this.boundingBox, mid = this.midpoint + ; + agents.sort(function(a,b){ + return Y.op.cmp( + manhattan(a.loc,mid), + manhattan(b.loc,mid) ); + }); + + return agents.attr(0); + }, + + willCollide : function willCollide(bullets, wiggle){ + wiggle = wiggle || 0; + var bb = this.boundingBox, mid = this.midpoint + , w = (bb.width+wiggle)/2, h = (bb.height+wiggle)/2 + ; + return bullets.filter(function(b){ + return !b.dead && b.willComeWithin(mid, w,h); + }); + }, + continueMove : function continueMove(){ if ( !this.currentMove || this.currentMoveLimit <= NOW ){ - var t = this.findNearLike(10000, 'Y.is(Tank, _) && _.align !== this.align').shift(); + var t = this.findNearEnemies(10000).shift(); this.calculatePath(t.midpoint); } @@ -189,18 +198,22 @@ Tank = Thing.subclass('Tank', { }, findNearLike : function findNearLike(ticks, fn){ - fn = fn.toFunction(); + fn = (fn || Y.op.K(true)).toFunction(); var bMovePerTick = MS_PER_FRAME * Bullet.prototype.stats.move*REF_SIZE/1000 , within = bMovePerTick*ticks , bb = this.boundingBox , x1 = bb.x1 - within, y1 = bb.y1 - within , x2 = bb.x2 + within, y2 = bb.y2 + within ; - return this.game.pathmap.get(x1,y1, x2,y2).filter(fn || Y.op.K(true), this); + return this.game.pathmap.get(x1,y1, x2,y2).filter(fn, this); + }, + + findNearEnemies : function findNearEnemies(ticks){ + return this.findNearLike(ticks, 'Y.is(Tank, _) && _.align !== this.align'); }, shoot : function shoot(x,y){ - var WIGGLE = 4 + var WIGGLE = 4 // additional space which must be clear in front of the barrel , xydef = (x !== undefined && y !== undefined) ; @@ -239,29 +252,34 @@ Tank = Thing.subclass('Tank', { return p; }, - onBulletDeath : function onBulletDeath(evt){ - this.nShots--; - }, - getTurretLoc : function getTurretLoc(){ var loc = this.loc - , barrel = this.barrel, bb = barrel.boundingBox - , theta = barrel.transform.rotate, sin = Math.sin(theta), cos = Math.cos(theta) - , x0 = 3+bb.x2-bb.x1, y0 = (bb.y2-bb.y1)/2 - , x = loc.x + bb.x1 + x0*cos - y0*sin - , y = loc.y + bb.y1 + x0*sin + y0*cos + , barrel = this.barrel, bbb = barrel.boundingBox + + , theta = barrel.transform.rotate + , sin = Math.sin(theta), cos = Math.cos(theta) + + , x0 = 3+bbb.x2-bbb.x1, y0 = (bbb.y2-bbb.y1)/2 + + , x = loc.x + bbb.x1 + x0*cos - y0*sin + , y = loc.y + bbb.y1 + x0*sin + y0*cos ; - // console.log('getTurretLoc()', 'loc:', loc, 'bb.(x2,y2):', [bb.x2,bb.y2], '(x,y):', [x,y]); + // console.log('getTurretLoc()', 'loc:', loc, 'bbb.(x2,y2):', [bbb.x2,bbb.y2], '(x,y):', [x,y]); return new math.Vec(x,y); }, move : function move(x,y){ var bb = this.boundingBox , w2 = bb.width/2, h2 = bb.height/2 + , x2 = x-w2, y2 = y-h2 ; - return this.moveByAngle( this.angleTo(x,y), x-w2,y-h2 ); + this.trajectory = new Trajectory(this, bb.x1,bb.y1, x2,y2, this.stats.move*REF_SIZE/1000); + return this.moveByAngle( this.angleTo(x,y), x2,y2 ); }, + /** + * @protected This method does not update this.trajectory -- call this.move(x,y) instead. + */ moveByAngle : function moveByAngle(theta, targetX,targetY){ var abs = Math.abs , bb = this.boundingBox, w2 = bb.width/2, h2 = bb.height/2 diff --git a/src/tanks/thing/thing.js b/src/tanks/thing/thing.js index 7cf9418..4fcd2ab 100644 --- a/src/tanks/thing/thing.js +++ b/src/tanks/thing/thing.js @@ -30,6 +30,7 @@ Thing = new Evt.Class('Thing', { blocking : true, // Whether the agent obstructs pathing active : true, // Whether the agent takes actions + // Location loc : null, boundingBox : null, @@ -42,7 +43,7 @@ Thing = new Evt.Class('Thing', { width : REF_SIZE*0.7, height : REF_SIZE*0.6, - set : Y.op.set.methodize(), + set : Y.op.set.methodize(), attr : Y.op.attr.methodize(), @@ -122,6 +123,24 @@ Thing = new Evt.Class('Thing', { return Math.atan2(y0,x0); }, + willComeWithin : function willComeWithin(pt, w,h){ + if ( !(this.trajectory && this.midpoint) ) + return false; + + if ( !Y.isNumber(w) ){ + h = w.height; w = w.width; + } + var trj = this.trajectory + , cur = this.midpoint + , fx = trj.calcX(pt.y), fy = trj.calcY(pt.x) + , t = trj.iparametric(cur.x, cur.y) + , ft = trj.iparametric(fx,fy) + , dw = Math.abs(fx - pt.x), dh = Math.abs(fy - pt.y) + ; + return ( t.x <= ft.x && t.y <= ft.y + && ( dw <= w || dh <= h ) ); + }, + // *** Gameplay Methods *** // @@ -134,9 +153,6 @@ Thing = new Evt.Class('Thing', { }, - - - /// Rendering Methods /// /** @@ -164,9 +180,8 @@ Y(Thing).extend({ THING_ID : 0, fillStats : function fillStats(stats){ - var st = Y(stats) - , stats = st.clone().end() - ; + var st = Y(stats) + , stats = st.clone().end() ; st.forEach(function(v, k){ k = Y(k);