/** * Actors represent something or someone, * and can consist of one or more states, * each associated with a particular sprite, * and each associated with particular * behaviour. * * The Actor class is abstract: you must * implement your own subclass before you * can make use of it. */ abstract class Actor extends Positionable { boolean debug = true; // debug bounding box alignment float halign=0, valign=0; // are we colliding with another actor? boolean colliding = false; // regular interaction with other actors boolean interacting = true; // only interact with players boolean onlyplayerinteraction = false; // bypass regular interaction for ... frames int disabledCounter = 0; // should we be removed? boolean remove = false; // is this actor persistent with respect to viewbox draws? boolean persistent = true; boolean isPersistent() { return persistent; } // the layer this actor is in LevelLayer layer; // The active state for this actor (with associated sprite) State active; // all states for this actor HashMap states; // actor name String name = ""; // simple constructor Actor(String _name) { name = _name; states = new HashMap(); } // full constructor Actor(String _name, float dampening_x, float dampening_y) { this(_name); setImpulseCoefficients(dampening_x, dampening_y); } /** * Add a state to this actor's repetoire. */ void addState(State state) { state.setActor(this); boolean replaced = (states.get(state.name) != null); states.put(state.name, state); if(!replaced || (replaced && state.name == active.name)) { if (active == null) { active = state; } else { swapStates(state); } updatePositioningInformation(); } } /** * Get a state by name. */ State getState(String name) { return states.get(name); } /** * Get the current sprite image */ PImage getSpriteMask() { if(active == null) return null; return active.sprite.getFrame(); } /** * Tell this actor which layer it is operating in */ void setLevelLayer(LevelLayer layer) { this.layer = layer; } /** * Tell this actor which layer it is operating in */ LevelLayer getLevelLayer() { return layer; } /** * Set the actor's current state by name. */ void setCurrentState(String name) { State tmp = states.get(name); if (active != null && tmp != active) { tmp.reset(); swapStates(tmp); } else { active = tmp; } } /** * Swap the current state for a different one. */ void swapStates(State tmp) { // get pertinent information Sprite osprite = active.sprite; boolean hflip = false, vflip = false; if (osprite != null) { hflip = osprite.hflip; vflip = osprite.vflip; } // upate state to new state active = tmp; Sprite nsprite = tmp.sprite; if (nsprite != null) { if (hflip) nsprite.flipHorizontal(); if (vflip) nsprite.flipVertical(); updatePositioningInformation(); // if both old and new states had sprites, // make sure the anchors line up. if (osprite != null) { handleSpriteSwap(osprite, nsprite); } } } /** * Move actor if this state changes * makes the actor bigger than before, * and we're attached to boundaries. */ void handleSpriteSwap(Sprite osprite, Sprite nsprite) { float ax1 = osprite.hanchor, ay1 = osprite.vanchor, ax2 = nsprite.hanchor, ay2 = nsprite.vanchor; float dx = (ax2-ax1)/2.0, dy = (ay2-ay1)/2.0; x -= dx; y -= dy; } /** * update the actor dimensions based * on the currently active state. */ void updatePositioningInformation() { width = active.sprite.width; height = active.sprite.height; halign = active.sprite.halign; valign = active.sprite.valign; } /** * constrain the actor position based on * the layer they are located in. */ void constrainPosition() { float w2 = width/2, lw = layer.width; if (x < w2) { x = w2; } if (x > lw - w2) { x = lw - w2; } } /** * Get the bounding box for this actor */ float[] getBoundingBox() { if(active==null) return null; float[] bounds = active.sprite.getBoundingBox(); // transform the bounds, based on local translation/scale/rotation if(r!=0) { float x1=bounds[0], y1=bounds[1], x2=bounds[2], y2=bounds[3], x3=bounds[4], y3=bounds[5], x4=bounds[6], y4=bounds[7]; // rotate bounds[0] = x1*cos(r) - y1*sin(r); bounds[1] = x1*sin(r) + y1*cos(r); bounds[2] = x2*cos(r) - y2*sin(r); bounds[3] = x2*sin(r) + y2*cos(r); bounds[4] = x3*cos(r) - y3*sin(r); bounds[5] = x3*sin(r) + y3*cos(r); bounds[6] = x4*cos(r) - y4*sin(r); bounds[7] = x4*sin(r) + y4*cos(r); } // translate bounds[0] += x+ox; bounds[1] += y+oy; // top left bounds[2] += x+ox; bounds[3] += y+oy; // top right bounds[4] += x+ox; bounds[5] += y+oy; // bottom right bounds[6] += x+ox; bounds[7] += y+oy; // bottom left // done return bounds; } /** * check overlap between sprites, * rather than between actors. */ float[] overlap(Actor other) { float[] overlap = super.overlap(other); if(overlap==null || active==null || other.active==null) { return overlap; } // // TODO: add in code here that determines // the intersection point for the two // sprites, and checks the mask to see // whether both have non-zero alph there. // return overlap; } /** * What happens when we touch another actor? */ void overlapOccurredWith(Actor other, float[] direction) { colliding = true; } /** * What happens when we get hit */ void hit() { /* can be overwritten */ } /** * attach an actor to a boundary, so that * impulse is redirected along boundary * surfaces. */ void attachTo(Boundary boundary, float[] correction) { // don't add boundaries we're already attached to if(boundaries.contains(boundary)) return; // record attachment boundaries.add(boundary); // stop the actor float[] original = {this.ix - (fx*ixF), this.iy - (fy*iyF)}; stop(correction[0], correction[1]); // then impart a new impulse, as redirected by the boundary. float[] rdf = boundary.redirectForce(original[0], original[1]); addImpulse(rdf[0], rdf[1]); // call the blocked handler gotBlocked(boundary, correction, original); // and then make sure to update the actor's position, as // otherwise it looks like we've stopped for 1 frame. update(); } /** * This boundary blocked our path. */ void gotBlocked(Boundary b, float[] intersection, float[] original) { // subclasses can implement, but don't have to } /** * collisions may force us to stop this * actor's movement. the actor is also * moved back by dx/dy */ void stop(float dx, float dy) { // we need to prevent IEEE floats polluting // the position information, so even though // the math is perfect in principle, round // the result so that we're not going to be // off by 0.0001 or something. float resolution = 50; x = int(resolution*(x+dx))/resolution; y = int(resolution*(y+dy))/resolution; ix = 0; iy = 0; aFrameCount = 0; } /** * Sometimes actors need to be "invulnerable" * while going through an animation. This * is achieved by setting "interacting" to false */ void setInteracting(boolean _interacting) { interacting = _interacting; } /** * set whether or not this actor interacts * with the level, or just the player */ void setPlayerInteractionOnly(boolean v ) { onlyplayerinteraction = v; } /** * Does this actor temporary not interact * with any Interactors? This function * is called by the layer level code, * and should not be called by anything else. */ boolean isDisabled() { if(disabledCounter > 0) { disabledCounter--; return true; } return false; } /** * Sometimes we need to bypass interaction for * a certain number of frames. */ void disableInteractionFor(int frameCount) { disabledCounter = frameCount; } /** * it's possible that an actor * has to be removed from the * level. If so, we call this method: */ void removeActor() { animated = false; visible = false; states = null; active = null; remove = true; } /** * Draw preprocessing happens here. */ void draw(float vx, float vy, float vw, float vh) { if(!remove) handleInput(); super.draw(vx,vy,vw,vh); } /** * Can this object be drawn in this viewbox? */ boolean drawableFor(float vx, float vy, float vw, float vh) { return true; } /** * Draw this actor. */ void drawObject() { if(active!=null) { active.draw(disabledCounter>0); /* if(debug) { noFill(); stroke(255,0,0); float[] bounds = getBoundingBox(); beginShape(); vertex(bounds[0]-x,bounds[1]-y); vertex(bounds[2]-x,bounds[3]-y); vertex(bounds[4]-x,bounds[5]-y); vertex(bounds[6]-x,bounds[7]-y); endShape(CLOSE); } */ } } // ====== KEY HANDLING ====== protected final boolean[] locked = new boolean[256]; protected final boolean[] keyDown = new boolean[256]; protected int[] keyCodes = {}; // if pressed, and part of our known keyset, mark key as "down" private void setIfTrue(int mark, int target) { if(!locked[target]) { if(mark==target) { keyDown[target] = true; }}} // if released, and part of our known keyset, mark key as "released" private void unsetIfTrue(int mark, int target) { if(mark==target) { locked[target] = false; keyDown[target] = false; }} // lock a key so that it cannot be triggered repeatedly protected void ignore(char key) { int keyCode = int(key); locked[keyCode] = true; keyDown[keyCode] = false; } // add a key listener protected void handleKey(char key) { int keyCode = int(key), len = keyCodes.length; int[] _tmp = new int[len+1]; arrayCopy(keyCodes,0,_tmp,0,len); _tmp[len] = keyCode; keyCodes = _tmp; } // check whether a key is pressed or not protected boolean isKeyDown(char key) { int keyCode = int(key); return keyDown[keyCode]; } protected boolean noKeysDown() { for(boolean b: keyDown) { if(b) return false; } for(boolean b: locked) { if(b) return false; } return true; } // handle key presses void keyPressed(char key, int keyCode) { for(int i: keyCodes) { setIfTrue(keyCode, i); }} // handle key releases void keyReleased(char key, int keyCode) { for(int i: keyCodes) { unsetIfTrue(keyCode, i); }} /** * Does the indicated x/y coordinate fall inside this drawable thing's region? */ boolean over(float _x, float _y) { if (active == null) return false; return active.over(_x - getX(), _y - getY()); } void mouseMoved(int mx, int my) {} void mousePressed(int mx, int my, int button) {} void mouseDragged(int mx, int my, int button) {} void mouseReleased(int mx, int my, int button) {} void mouseClicked(int mx, int my, int button) {} // ====== ABSTRACT METHODS ====== // token implementation void handleInput() { } // token implementation void handleStateFinished(State which) { } // token implementation void pickedUp(Pickup pickup) { } } /** * Boundaries are unidirectionally passable, * and are positionable in the same way that * anything else is. */ class Boundary extends Positionable { private float PI2 = 2*PI; // things can listen for collisions on this boundary ArrayList listeners; /** * Add a collision listener to this boundary */ void addListener(BoundaryCollisionListener l) { listeners.add(l); } /** * remove a collision listener from this boundary */ void removeListener(BoundaryCollisionListener l) { listeners.remove(l); } /** * notify all listners that a collision occurred. */ void notifyListeners(Actor actor, float[] correction) { for(BoundaryCollisionListener l: listeners) { l.collisionOccured(this, actor, correction); } } // extended adminstrative values float dx, dy, length; float xw, yh; float minx, maxx, miny, maxy; float angle, cosa, sina, cosma, sinma; // <1 means friction, =1 means frictionless, >1 means speed boost! float glide; // boundaries can be linked Boundary prev, next; float boundingThreshold = 1.5; boolean disabled = false; /** * When we build a boundary, we record a * vast number of shortcut values so we * don't need to recompute them constantly. */ Boundary(float x1, float y1, float x2, float y2) { // coordinates x = x1; y = y1; xw = x2; yh = y2; // deltas dx = x2-x1; dy = y2-y1; length = sqrt(dx*dx+dy*dy); updateBounds(); updateAngle(); glide = 1.0; listeners = new ArrayList(); } /** * Update our bounding box information */ void updateBounds() { xw = x + dx; yh = y + dy; minx = min(x, xw); maxx = max(x, xw); miny = min(y, yh); maxy = max(y, yh); } /** * Update our angle in the world */ void updateAngle() { angle = atan2(dy, dx); if (angle < 0) angle += 2*PI; cosma = cos(-angle); sinma = sin(-angle); cosa = cos(angle); sina = sin(angle); } void setPosition(float _x, float _y) { super.setPosition(_x,_y); updateBounds(); } void moveBy(float dx, float dy) { super.moveBy(dx,dy); updateBounds(); } /** * This boundary is part of a chain, and * the previous boundary is: */ void setPrevious(Boundary b) { prev = b; } /** * This boundary is part of a chain, and * the next boundary is: */ void setNext(Boundary b) { next = b; } /** * Enable this boundary */ void enable() { disabled = false; } /** * Disable this boundary */ void disable() { disabled = true; } /** * Is this positionable actually * supported by this boundary? */ // FIXME: this is not the correct implementation boolean supports(Positionable thing) { float[] bbox = thing.getBoundingBox(), nbox = new float[8]; // shortcut on "this thing has already been removed" if (bbox == null) return false; // First, translate all coordinates so that they're // relative to the boundary's (x,y) coordinate. bbox[0] -= x; bbox[1] -= y; bbox[2] -= x; bbox[3] -= y; bbox[4] -= x; bbox[5] -= y; bbox[6] -= x; bbox[7] -= y; // Then, rotate the bounding box so that it's // axis-aligned with the boundary line. nbox[0] = bbox[0] * cosma - bbox[1] * sinma; nbox[1] = bbox[0] * sinma + bbox[1] * cosma; nbox[2] = bbox[2] * cosma - bbox[3] * sinma; nbox[3] = bbox[2] * sinma + bbox[3] * cosma; nbox[4] = bbox[4] * cosma - bbox[5] * sinma; nbox[5] = bbox[4] * sinma + bbox[5] * cosma; nbox[6] = bbox[6] * cosma - bbox[7] * sinma; nbox[7] = bbox[6] * sinma + bbox[7] * cosma; // Get new bounding box minima/maxima float mx = min(min(nbox[0],nbox[2]),min(nbox[4],nbox[6])), MX = max(max(nbox[0],nbox[2]),max(nbox[4],nbox[6])), my = min(min(nbox[1],nbox[3]),min(nbox[5],nbox[7])), MY = max(max(nbox[1],nbox[3]),max(nbox[5],nbox[7])); // Now, determine whether we're "off" the boundary... boolean outOfBounds = (mx > length) || (MX < 0) || (MY<-1.99); // if the thing's not out of bounds, it's supported. return !outOfBounds; } /** * If our direction of travel goes through the boundary in * the "allowed" direction, don't bother collision detection. */ boolean allowPassThrough(float ix, float iy) { float[] aligned = CollisionDetection.translateRotate(0,0,ix,iy, 0,0,dx,dy, angle,cosma,sinma); return (aligned[3] < 0); } /** * redirect a force along this boundary's surface. */ float[] redirectForce(float fx, float fy) { float[] redirected = {fx,fy}; if(allowPassThrough(fx,fy)) { return redirected; } float[] tr = CollisionDetection.translateRotate(0,0,fx,fy, 0,0,dx,dy, angle,cosma,sinma); redirected[0] = glide * tr[2] * cosa; redirected[1] = glide * tr[2] * sina; return redirected; } /** * redirect a force along this boundary's surface for a specific actor */ float[] redirectForce(Positionable p, float fx, float fy) { return redirectForce(fx,fy); } /** * Can this object be drawn in this viewbox? */ boolean drawableFor(float vx, float vy, float vw, float vh) { // boundaries are invisible to begin with. return true; } /** * draw this platform */ void drawObject() { strokeWeight(1); stroke(255); line(0, 0, dx, dy); // draw an arrow to indicate the pass-through direction float cs = cos(angle-PI/2), ss = sin(angle-PI/2); float fx = 10*cs; float fy = 10*ss; line((dx-fx)/2, (dy-fy)/2, dx/2 + fx, dy/2 + fy); float fx2 = 6*cs - 4*ss; float fy2 = 6*ss + 4*cs; line(dx/2+fx2, dy/2+fy2, dx/2 + fx, dy/2 + fy); fx2 = 6*cs + 4*ss; fy2 = 6*ss - 4*cs; line(dx/2+fx2, dy/2+fy2, dx/2 + fx, dy/2 + fy); } /** * Useful for debugging */ String toString() { return x+","+y+","+xw+","+yh; } } /** * Things can listen to boundary collisions for a boundary */ interface BoundaryCollisionListener { void collisionOccured(Boundary boundary, Actor actor, float[] intersectionInformation); }/** * A bounded interactor is a normal Interactor with * one or more boundaries associated with it. */ abstract class BoundedInteractor extends Interactor implements BoundaryCollisionListener { // the list of associated boundaries ArrayList boundaries; // are the boundaries active? boolean bounding = true; // simple constructor BoundedInteractor(String name) { this(name,0,0); } // full constructor BoundedInteractor(String name, float dampening_x, float dampening_y) { super(name, dampening_x, dampening_y); boundaries = new ArrayList(); } // add a boundary void addBoundary(Boundary boundary) { boundary.setImpulseCoefficients(ixF,iyF); boundaries.add(boundary); } // add a boundary, and register as listener for collisions on it void addBoundary(Boundary boundary, boolean listen) { addBoundary(boundary); boundary.addListener(this); } // FIXME: make this make sense, because setting 'next' // should only work on open-bounded interactors. void setNext(BoundedInteractor next) { if(boundaries.size()==1) { boundaries.get(0).setNext(next.boundaries.get(0)); } } // FIXME: make this make sense, because setting 'previous' // should only work on open-bounded interactors. void setPrevious(BoundedInteractor prev) { if(boundaries.size()==1) { boundaries.get(0).setPrevious(prev.boundaries.get(0)); } } // enable all boundaries void enableBoundaries() { bounding = true; for(Boundary b: boundaries) { b.enable(); } } // disable all boundaries void disableBoundaries() { bounding = false; for(int b=boundaries.size()-1; b>=0; b--) { Boundary boundary = boundaries.get(b); boundary.disable(); } } /** * We must make sure to remove all * boundaries when we are removed. */ void removeActor() { disableBoundaries(); boundaries = new ArrayList(); super.removeActor(); } // draw boundaries void drawBoundaries(float x, float y, float w, float h) { for(Boundary b: boundaries) { b.draw(x,y,w,h); } } /** * Is something attached to one of our boundaries? */ boolean havePassenger() { // no passengers return false; } /** * listen to collisions on bounded boundaries */ abstract void collisionOccured(Boundary boundary, Actor actor, float[] intersectionInformation); // when we update our coordinates, also // update our associated boundaries. void update() { super.update(); // how much did we actually move? float dx = x-previous.x; float dy = y-previous.y; // if it's not 0, move the boundaries if(dx!=0 && dy!=0) { for(Boundary b: boundaries) { // FIXME: somehow this goes wrong when the // interactor is contrained by another // boundary, where the actor moves, but the // associated boundary for some reason doesn't. b.moveBy(dx,dy); } } } } /** * Alternative collision detection */ static class CollisionDetection { private static boolean debug = false; /** * Static classes need global sketch binding */ private static PApplet sketch; public static void init(PApplet s) { sketch = s; } /** * Perform actor/boundary collision detection */ static void interact(Boundary b, Actor a) { // no interaction if actor was removed from the game. if (a.remove) return; // no interaction if actor has not moved. if (a.x == a.previous.x && a.y == a.previous.y) return; float[] correction = blocks(b,a); if(correction != null) { b.notifyListeners(a, correction); a.attachTo(b, correction); } } /** * Is this boundary blocking the specified actor? */ static float[] blocks(Boundary b, Actor a) { float[] current = a.getBoundingBox(), previous = a.previous.getBoundingBox(), line = {b.x, b.y, b.xw, b.yh}; return CollisionDetection.getLineRectIntersection(line, previous, current); } /** * Perform line/rect intersection detection. Lines represent boundaries, * and rather than doing "normal" line/rect intersection using a * "box on a trajectory" that normal actor movement looks like, we pretend * the actor box remains stationary, and move the boundary in the opposite * direction with the same speed, which gives us a "boundary box", so that * we can perform box/box overlap detection instead. */ static float[] getLineRectIntersection(float[] line, float[] previous, float[] current) { if(debug) sketch.println(sketch.frameCount + " ***"); if(debug) sketch.println(sketch.frameCount + "> testing against: "+arrayToString(line)); if(debug) sketch.println(sketch.frameCount + "> previous: "+arrayToString(previous)); if(debug) sketch.println(sketch.frameCount + "> current : "+arrayToString(current)); // First, let's do some dot-product math, to find out whether or not // the actor's bounding box is even in range of the boundary. float x1=line[0], y1=line[1], x2=line[2], y2=line[3], fx = current[0] - previous[0], fy = current[1] - previous[1], pv=PI/2.0, dx = x2-x1, dy = y2-y1, rdx = dx*cos(pv) - dy*sin(pv), rdy = dx*sin(pv) + dy*cos(pv); // is the delta in a permitted direction? If so, we don't have to do // intersection detection because there won't be any. float dotproduct = getDotProduct(rdx, rdy, fx, fy); if(dotproduct<0) { return null; } // then: in-range checks. If not in range, no need to do the more // complicated intersection detections checks. // determine range w.r.t. the starting point of the boundary. float[] dotProducts_S_P = getDotProducts(x1,y1,x2,y2, previous); float[] dotProducts_S_C = getDotProducts(x1,y1,x2,y2, current); // determine range w.r.t. the end point of the boundary. float[] dotProducts_E_P = getDotProducts(x2,y2,x1,y1, previous); float[] dotProducts_E_C = getDotProducts(x2,y2,x1,y1, current); // determine 'sidedness', relative to the boundary. float[] dotProducts_P = getDotProducts(x1,y1,x1+rdx,y1+rdy, previous); float[] dotProducts_C = getDotProducts(x1,y1,x1+rdx,y1+rdy, current); // compute the relevant feature values based on the dot products: int inRangeSp = 4, inRangeSc = 4, inRangeEp = 4, inRangeEc = 4, abovePrevious = 0, aboveCurrent = 0; for(int i=0; i<8; i+=2) { if (dotProducts_S_P[i] < 0) { inRangeSp--; } if (dotProducts_S_C[i] < 0) { inRangeSc--; } if (dotProducts_E_P[i] < 0) { inRangeEp--; } if (dotProducts_E_C[i] < 0) { inRangeEc--; } if (dotProducts_P[i] <= 0) { abovePrevious++; } if (dotProducts_C[i] <= 0) { aboveCurrent++; }} if(debug) sketch.println(sketch.frameCount +"> dotproduct result: start="+inRangeSp+"/"+inRangeSc+", end="+inRangeEp+"/"+inRangeEc+", sided="+abovePrevious+"/"+aboveCurrent); // make sure to short-circuit if the actor cannot // interact with the boundary because it is out of range. boolean inRangeForStart = (inRangeSp == 0 && inRangeSc == 0); boolean inRangeForEnd = (inRangeEp == 0 && inRangeEc == 0); if (inRangeForStart || inRangeForEnd) { if(debug) sketch.println(sketch.frameCount +"> this boundary is not involved in collisions for this frame (out of range)."); return null; } // if the force goes against the border's permissible direction, but // both previous and current frame actor boxes are above the boundary, // then we don't have to bother with intersection detection. if (abovePrevious==4 && aboveCurrent==4) { if(debug) sketch.println(sketch.frameCount +"> this box is not involved in collisions for this frame (inherently safe 'above' locations)."); return null; } else if(0 < abovePrevious && abovePrevious < 4) { if(debug) sketch.println(sketch.frameCount +"> this box is not involved in collisions for this frame (never fully went through boundary)."); return null; } // Now then, let's determine whether overlap will occur. boolean found = false; // We're in bounds: if 'above' is 4, meaning that our previous // actor frame is on the blocking side of a boundary, and // 'aboveAfter' is 0, meaning its current frame is on the other // side of the boundary, then a collision MUST have occurred. if (abovePrevious==4 && aboveCurrent==0) { // note that in this situation, the overlap may look // like full containment, where the actor's bounding // box is fully contained by the boundary's box. found = true; if(debug) sketch.println(sketch.frameCount +"> collision detected (due to full containment)."); } else { // We're in bounds: do box/box intersection checking // using the 'previous' box and the boundary-box. dx = previous[0] - current[0]; dy = previous[1] - current[1]; // form boundary box float[] bbox = {line[0], line[1], line[2], line[3], line[2]+dx, line[3]+dy, line[0]+dx, line[1]+dy}; // do any of the "previous" edges intersect // with any of the "boundary box" edges? int i,j; float[] p = previous, b = bbox, intersection; for(i=0; i<8; i+=2) { for(j=0; j<8; j+=2) { intersection = getLineLineIntersection(p[i], p[i+1], p[(i+2)%8], p[(i+3)%8], b[j], b[j+1], b[(j+2)%8], b[(j+3)%8], false, true); if (intersection != null) { found = true; if(debug) sketch.println(sketch.frameCount +"> collision detected on a box edge (box overlap)."); } } } } // Have we signaled any overlap? if (found) { float[] distances = getCornerDistances(x1,y1,x2,y2, previous, current); int[] corners = rankCorners(distances); if(debug) { sketch.print(sketch.frameCount + "> "); for(int i=0; i<4; i++) { sketch.print(corners[i]+"="+distances[corners[i]]); if(i<3) sketch.print(", "); } sketch.println(); } // Get the corner on the previous and current actor bounding // box that will "hit" the boundary first. int corner = 0; float xp = previous[corners[corner]], yp = previous[corners[corner]+1], xc = current[corners[corner]], yc = current[corners[corner]+1]; // The trajectory for this point may intersect with // the boundary. If it does, we'll have all the information // we need to move the actor back along its trajectory by // an amount that will place it "on" the boundary, at the right spot. float[] intersection = getLineLineIntersection(xp,yp,xc,yc, x1,y1,x2,y2, false, true); if (intersection==null) { if(debug) println("nearest-to-boundary is actually not on the boundary itself. More complex math is required!"); // it's also possible that the first corner to hit the boundary // actually never touches the boundary because it intersects only // if the boundary is infinitely long. So... let's make that happen: intersection = getLineLineIntersection(xp,yp,xc,yc, x1,y1,x2,y2, false, false); if (intersection==null) { if(debug) println("line extension alone is not enoough..."); // FIXME: this is not satisfactory! A real solution should be implemented! return new float[]{xp-xc, yp-yc}; // effect a full rewind for now } return new float[]{intersection[0] - xc, intersection[1] - yc}; } // if we get here, there was a normal trajectory // intersection with the boundary. Computing the // corrective values by which to move the current // frame's bounding box is really simple: dx = intersection[0] - xc; dy = intersection[1] - yc; if(debug) sketch.println(sketch.frameCount +"> dx: "+dx+", dy: "+dy); return new float[]{dx, dy}; } return null; } /** * For each corner in an object's bounding box, get the distance from its "previous" * box to the line defined by (x1,y1,x2,y2). Return this as float[8], corresponding * to the bounding box array format. */ static float[] getCornerDistances(float x1, float y1, float x2, float y2, float[] previous, float[] current) { float[] distances = {0,0,0,0,0,0,0,0}, intersection; float dx, dy; for(int i=0; i<8; i+=2) { intersection = getLineLineIntersection(x1,y1,x2,y2, previous[i], previous[i+1], current[i], current[i+1], false, false); if (intersection == null) { continue; } dx = intersection[0] - previous[i]; dy = intersection[1] - previous[i+1]; distances[i] = sqrt(dx*dx+dy*dy); distances[i+1] = distances[i]; } return distances; } /** * Get the intersection coordinate between two lines segments, * using fairly standard, if a bit lenghty, linear algebra. */ static float[] getLineLineIntersection(float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4, boolean colinearity, boolean segments) { float epsilon = 0.1; // convert lines to the generatised form [a * x + b + y = c] float a1 = -(y2 - y1), b1 = (x2 - x1), c1 = (x2 - x1) * y1 - (y2 - y1) * x1; float a2 = -(y4 - y3), b2 = (x4 - x3), c2 = (x4 - x3) * y3 - (y4 - y3) * x3; // find their intersection float d = a1 * b2 - a2 * b1; if (d == 0) { // Two lines are parallel: we are not interested in the // segment if the points are not colinear. if (!colinearity || (x2 - x3) * (y2 - y1) != (y2 - y3) * (x2 - x1)) { return null; } // Solve the algebraic functions [x = (x1 - x0) * t + x0] for t float t1 = x3 != x4 ? (x1 - x3) / (x4 - x3) : (y1 - y3) / (y4 - y3); float t2 = x3 != x4 ? (x2 - x3) / (x4 - x3) : (y2 - y3) / (y4 - y3); if ((t1 < 0 && t2 < 0) || (t1 > 1 && t2 > 1)) { // points 1 and 2 are outside the points 3 and 4 segment return null; } // Clamp t values to the interval [0, 1] t1 = t1 < 0 ? 0 : t1 > 1 ? 1 : t1; t2 = t2 < 0 ? 0 : t2 > 1 ? 1 : t2; return new float[]{(x4 - x3) * t1 + x3, (y4 - y3) * t1 + y3, (x4 - x3) * t2 + x3, (y4 - y3) * t2 + y3}; } // not colinear - find the intersection point else { float x = (c1 * b2 - c2 * b1) / d; float y = (a1 * c2 - a2 * c1) / d; // make sure the point can be found on both segments. if (segments && (x < min(x1, x2) - epsilon || max(x1, x2) + epsilon < x || y < min(y1, y2) - epsilon || max(y1, y2) + epsilon < y || x < min(x3, x4) - epsilon || max(x3, x4) + epsilon < x || y < min(y3, y4) - epsilon || max(y3, y4) + epsilon < y)) { // not on either, or both, segments. return null; } return new float[]{x, y}; } } /** * compute the dot product between all corner points of a * bounding box, and a boundary line with origin ox/oy * and end point tx/ty. */ static float[] getDotProducts(float ox, float oy, float tx, float ty, float[] bbox) { float dotx = tx-ox, doty = ty-oy, dx, dy, len, dotproduct; float[] dotProducts = new float[8]; for(int i=0; i<8; i+=2) { dx = bbox[i]-ox; dy = bbox[i+1]-oy; dotproduct = getDotProduct(dotx,doty, dx, dy); dotProducts[i] = dotproduct; } return dotProducts; } /** * get the dot product between two vectors */ static float getDotProduct(float dx1, float dy1, float dx2, float dy2) { // normalise both vectors float l1 = sqrt(dx1*dx1 + dy1*dy1), l2 = sqrt(dx2*dx2 + dy2*dy2); if (l1==0 || l2==0) return 0; dx1 /= l1; dy1 /= l1; dx2 /= l2; dy2 /= l2; return dx1*dx2 + dy1*dy2; } /** * Rank the corner points for a bounding box * based on it's distance to the boundary, * along its trajectory path. * * We rank by decreasing distance. */ // FIXME: this is a pretty fast code, but there might be // better ways to achieve the desired result. static int[] rankCorners(float[] distances) { int[] corners = {0,0,0,0}; float corner1v=999999, corner2v=corner1v, corner3v=corner1v, corner4v=corner1v, distance; int corner1=-1, corner2=-1, corner3=-1, corner4=-1; for(int i=0; i<8; i+=2) { distance = distances[i]; if (distance < corner1v) { corner4v = corner3v; corner4 = corner3; corner3v = corner2v; corner3 = corner2; corner2v = corner1v; corner2 = corner1; corner1v = distance; corner1 = i; continue; } if (distance < corner2v) { corner4v = corner3v; corner4 = corner3; corner3v = corner2v; corner3 = corner2; corner2v = distance; corner2 = i; continue; } if (distance < corner3v) { corner4v = corner3v; corner4 = corner3; corner3v = distance; corner3 = i; continue; } corner4v = distance; corner4 = i; } // Set up the corners, ranked by // proximity to the boundary. corners[0] = corner1; corners[1] = corner2; corners[2] = corner3; corners[3] = corner4; return corners; } /** * Check if a bounding box's dot product * information implies it's safe, or blocked. */ static boolean permitted(float[] dotProducts) { for(int i=0; i<8; i+=2) { if (dotProducts[i]>0) return true; } return false; } /** * Perform a coordinate tranlation/rotation so that * line (x3/y3, x4/y4) becomes (0/0, .../0), with * the other coordinates transformed accordingly * * returns float[9], with [0]/[1], [2]/[3], [4]/[5] and [6]/[7] * being four coordinate pairs, and [8] being * the angle of rotation used by this transform. */ static float[] translateRotate(float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4, float angle, float cosine, float sine) { // First, translate all coordinates so that x3/y3 lies on 0/0 x1 -= x3; y1 -= y3; x2 -= x3; y2 -= y3; x4 -= x3; y4 -= y3; // Rotate (x1'/y1') about (0,0) float x1n = x1 * cosine - y1 * sine, y1n = x1 * sine + y1 * cosine; // Rotate (x2'/y2') about (0,0) float x2n = x2 * cosine - y2 * sine, y2n = x2 * sine + y2 * cosine; // Rotate (x4'/y4') about (0,0) float x4n = x4 * cosine - y4 * sine; // And then return the transformed coordinates, plus angle used return new float[] {x1n, y1n, x2n, y2n, 0, 0, x4n, 0, angle}; } /** * Simple drawing helper function */ static void drawBox(float[] boundingbox) { sketch.line(boundingbox[0], boundingbox[1], boundingbox[2], boundingbox[3]); sketch.line(boundingbox[2], boundingbox[3], boundingbox[4], boundingbox[5]); sketch.line(boundingbox[4], boundingbox[5], boundingbox[6], boundingbox[7]); sketch.line(boundingbox[6], boundingbox[7], boundingbox[0], boundingbox[1]); } /** * Simple printing helper function */ static String arrayToString(float[] arr) { String str = ""; for(int i=0; i0) { super.draw(); } else { remove = true; } } } /** * Any class that implements this interface * is able to draw itself ONLY when its visible * regions are inside the indicated viewbox. */ interface Drawable { /** * draw this thing, as long as it falls within the drawbox defined by x/y -- x+w/y+h */ void draw(float x, float y, float w, float h); } /** * This encodes all the boilerplate code * necessary for screen drawing and input * handling. */ // global screens container HashMap screenSet; // global 'currently active' screen Screen activeScreen = null; // setup sets up the screen size, and screen container, // then calls the "initialize" method, which you must // implement yourself. void setup() { size(screenWidth, screenHeight); noLoop(); screenSet = new HashMap(); SpriteMapHandler.init(this); SoundManager.init(this); CollisionDetection.init(this); initialize(); } // draw loop void draw() { activeScreen.draw(); SoundManager.draw(); } // event handling void keyPressed() { activeScreen.keyPressed(key, keyCode); } void keyReleased() { activeScreen.keyReleased(key, keyCode); } void mouseMoved() { activeScreen.mouseMoved(mouseX, mouseY); } void mousePressed() { SoundManager.clicked(mouseX,mouseY); activeScreen.mousePressed(mouseX, mouseY, mouseButton); } void mouseDragged() { activeScreen.mouseDragged(mouseX, mouseY, mouseButton); } void mouseReleased() { activeScreen.mouseReleased(mouseX, mouseY, mouseButton); } void mouseClicked() { activeScreen.mouseClicked(mouseX, mouseY, mouseButton); } /** * Mute the game */ void mute() { SoundManager.mute(true); } /** * Unmute the game */ void unmute() { SoundManager.mute(false); } /** * Screens are added to the game through this function. */ void addScreen(String name, Screen screen) { screenSet.put(name, screen); if (activeScreen == null) { activeScreen = screen; loop(); } else { SoundManager.stop(activeScreen); } } /** * We switch between screens with this function. * * Because we might want to move things from the * old screen to the new screen, this function gives * you a reference to the old screen after switching. */ Screen setActiveScreen(String name) { Screen oldScreen = activeScreen; activeScreen = screenSet.get(name); if (oldScreen != null) { oldScreen.cleanUp(); SoundManager.stop(oldScreen); } SoundManager.loop(activeScreen); return oldScreen; } /** * Screens can be removed to save memory, etc. * as long as they are not the active screen. */ void removeScreen(String name) { if (screenSet.get(name) != activeScreen) { screenSet.remove(name); } } /** * Get a specific screen (for debug purposes) */ Screen getScreen(String name) { return screenSet.get(name); } /** * clear all screens */ void clearScreens() { screenSet = new HashMap(); activeScreen = null; } /** * Interactors are non-player actors * that can interact with other interactors * as well as player actors. However, * they do not interact with pickups. */ abstract class Interactor extends Actor { // simple constructor Interactor(String name) { super(name); } // full constructor Interactor(String name, float dampening_x, float dampening_y) { super(name, dampening_x, dampening_y); } /** * Can this object be drawn in this viewbox? */ boolean drawableFor(float vx, float vy, float vw, float vh) { return persistent || (vx-vw <= x && x <= vx+2*vw && vy-vh <= y && y <=vy+2*vh); } // Interactors don't do anything with pickups by default void pickedUp(Pickup pickup) {} // Interactors are not playable final void handleInput() {} } /** * JavaScript interface, to enable console.log */ interface JSConsole { void log(String msg); } /** * Abstract JavaScript class, containing * a console object (with a log() method) * and access to window.setPaths(). */ abstract class JavaScript { JSConsole console; abstract void loadInEditor(Positionable thing); abstract boolean shouldMonitor(); abstract void updatedPositionable(Positionable thing); abstract void reset(); } /** * Local reference to the javascript environment */ JavaScript javascript; /** * Binding function used by JavaScript to bind * the JS environment to the sketch. */ void bindJavaScript(JavaScript js) { javascript = js; } /** * This class defines a generic sprite engine level. * A layer may consist of one or more layers, with * each layer modeling a 'self-contained' slice. * For top-down games, these slices yield pseudo-height, * whereas for side-view games they yield pseudo-depth. */ abstract class Level extends Screen { boolean finished = false; ArrayList layers; HashMap layerids; // current viewbox ViewBox viewbox; /** * Levels have dimensions! */ Level(float _width, float _height) { super(_width,_height); layers = new ArrayList(); layerids = new HashMap(); viewbox = new ViewBox(_width, _height); } /** * The viewbox only shows part of the level, * so that we don't waste time computing things * for parts of the level that we can't even see. */ void setViewBox(float _x, float _y, float _w, float _h) { viewbox.x = _x; viewbox.y = _y; viewbox.w = _w; viewbox.h = _h; } void addLevelLayer(String name, LevelLayer layer) { layerids.put(name,layers.size()); layers.add(layer); } LevelLayer getLevelLayer(String name) { return layers.get(layerids.get(name)); } void cleanUp() { for(LevelLayer l: layers) { l.cleanUp(); } } // FIXME: THIS IS A TEST FUNCTION. KEEP? REJECT? void updatePlayer(Player oldPlayer, Player newPlayer) { for(LevelLayer l: layers) { l.updatePlayer(oldPlayer, newPlayer); } } /** * Change the behaviour when the level finishes */ void finish() { setSwappable(); finished = true; } /** * What to do on a premature level finish (for instance, a reset-warranting death) */ void end() { finish(); } /** * draw the level, as seen from the viewbox */ void draw() { translate(-viewbox.x, -viewbox.y); for(LevelLayer l: layers) { l.draw(); } } // used for statistics int getActorCount() { int count = 0; for(LevelLayer l: layers) { count += l.getActorCount(); } return count; } /** * passthrough events */ void keyPressed(char key, int keyCode) { for(LevelLayer l: layers) { l.keyPressed(key, keyCode); } } void keyReleased(char key, int keyCode) { for(LevelLayer l: layers) { l.keyReleased(key, keyCode); } } void mouseMoved(int mx, int my) { for(LevelLayer l: layers) { l.mouseMoved(mx, my); } } void mousePressed(int mx, int my, int button) { for(LevelLayer l: layers) { l.mousePressed(mx, my, button); } } void mouseDragged(int mx, int my, int button) { for(LevelLayer l: layers) { l.mouseDragged(mx, my, button); } } void mouseReleased(int mx, int my, int button) { for(LevelLayer l: layers) { l.mouseReleased(mx, my, button); } } void mouseClicked(int mx, int my, int button) { for(LevelLayer l: layers) { l.mouseClicked(mx, my, button); } } // layer component show/hide methods void showBackground(boolean b) { for(LevelLayer l: layers) { l.showBackground = b; }} void showBoundaries(boolean b) { for(LevelLayer l: layers) { l.showBoundaries = b; }} void showPickups(boolean b) { for(LevelLayer l: layers) { l.showPickups = b; }} void showDecals(boolean b) { for(LevelLayer l: layers) { l.showDecals = b; }} void showInteractors(boolean b) { for(LevelLayer l: layers) { l.showInteractors = b; }} void showActors(boolean b) { for(LevelLayer l: layers) { l.showActors = b; }} void showForeground(boolean b) { for(LevelLayer l: layers) { l.showForeground = b; }} void showTriggers(boolean b) { for(LevelLayer l: layers) { l.showTriggers = b; }} } /** * Level layers are intended to regulate both interaction * (actors on one level cannot affect actors on another) * as well as to effect pseudo-depth. * * Every layer may contain the following components, * drawn in the order listed: * * - a background sprite layer * - (actor blocking) boundaries * - pickups (extensions on actors) * - non-players (extensions on actors) * - player actors (extension on actors) * - a foreground sprite layer * */ abstract class LevelLayer { // debug flags, very good for finding out what's going on. boolean debug = true, showBackground = true, showBoundaries = false, showPickups = true, showDecals = true, showInteractors = true, showActors = true, showForeground = true, showTriggers = false; // The various layer components ArrayList boundaries; ArrayList fixed_background, fixed_foreground; ArrayList pickups; ArrayList npcpickups; ArrayList decals; ArrayList interactors; ArrayList bounded_interactors; ArrayList players; ArrayList triggers; // Level layers need not share the same coordinate system // as the managing level. For instance, things in the // background might be rendered smaller to seem farther // away, or larger, to create an exxagerated look. float xTranslate = 0, yTranslate = 0, xScale = 1, yScale = 1; boolean nonstandard = false; // Fallback color if a layer has no background color. // By default, this color is 100% transparent black. color backgroundColor = -1; void setBackgroundColor(color c) { backgroundColor = c; } // the list of "collision" regions void addBoundary(Boundary boundary) { boundaries.add(boundary); } void removeBoundary(Boundary boundary) { boundaries.remove(boundary); } void clearBoundaries() { boundaries.clear(); } // The list of static, non-interacting sprites, building up the background void addBackgroundSprite(Drawable fixed) { fixed_background.add(fixed); } void removeBackgroundSprite(Drawable fixed) { fixed_background.remove(fixed); } void clearBackground() { fixed_background.clear(); } // The list of static, non-interacting sprites, building up the foreground void addForegroundSprite(Drawable fixed) { fixed_foreground.add(fixed); } void removeForegroundSprite(Drawable fixed) { fixed_foreground.remove(fixed); } void clearForeground() { fixed_foreground.clear(); } // The list of decals (pure graphic visuals) void addDecal(Decal decal) { decals.add(decal); } void removeDecal(Decal decal) { decals.remove(decal); } void clearDecals() { decals.clear(); } // event triggers void addTrigger(Trigger trigger) { triggers.add(trigger); } void removeTrigger(Trigger trigger) { triggers.remove(trigger); } void clearTriggers() { triggers.clear(); } // The list of sprites that may only interact with the player(s) (and boundaries) void addForPlayerOnly(Pickup pickup) { pickups.add(pickup); bind(pickup); } void removeForPlayerOnly(Pickup pickup) { pickups.remove(pickup); } void clearPickups() { pickups.clear(); npcpickups.clear(); } // The list of sprites that may only interact with non-players(s) (and boundaries) void addForInteractorsOnly(Pickup pickup) { npcpickups.add(pickup); bind(pickup); } void removeForInteractorsOnly(Pickup pickup) { npcpickups.remove(pickup); } // The list of fully interacting non-player sprites void addInteractor(Interactor interactor) { interactors.add(interactor); bind(interactor); } void removeInteractor(Interactor interactor) { interactors.remove(interactor); } void clearInteractors() { interactors.clear(); bounded_interactors.clear(); } // The list of fully interacting non-player sprites that have associated boundaries void addBoundedInteractor(BoundedInteractor bounded_interactor) { bounded_interactors.add(bounded_interactor); bind(bounded_interactor); } void removeBoundedInteractor(BoundedInteractor bounded_interactor) { bounded_interactors.remove(bounded_interactor); } // The list of player sprites void addPlayer(Player player) { players.add(player); bind(player); } void removePlayer(Player player) { players.remove(player); } void clearPlayers() { players.clear(); } void updatePlayer(Player oldPlayer, Player newPlayer) { int pos = players.indexOf(oldPlayer); if (pos > -1) { players.set(pos, newPlayer); newPlayer.boundaries.clear(); bind(newPlayer); }} // private actor binding void bind(Actor actor) { actor.setLevelLayer(this); } // clean up all transient things void cleanUp() { cleanUpActors(interactors); cleanUpActors(bounded_interactors); cleanUpActors(pickups); cleanUpActors(npcpickups); } // cleanup an array list void cleanUpActors(ArrayList list) { for(int a = list.size()-1; a>=0; a--) { if(!list.get(a).isPersistent()) { list.remove(a); } } } // clear everything except the player void clearExceptPlayer() { clearBoundaries(); clearBackground(); clearForeground(); clearDecals(); clearTriggers(); clearPickups(); clearInteractors(); } // clear everything void clear() { clearExceptPlayer(); clearPlayers(); } // =============================== // // MAIN CLASS CODE STARTS HERE // // =============================== // // level layer size float width=0, height=0; // the owning level for this layer Level parent; // level viewbox ViewBox viewbox; /** * fallthrough constructor */ LevelLayer(Level p) { this(p, p.width, p.height); } /** * Constructor */ LevelLayer(Level p, float w, float h) { this.parent = p; this.viewbox = p.viewbox; this.width = w; this.height = h; boundaries = new ArrayList(); fixed_background = new ArrayList(); fixed_foreground = new ArrayList(); pickups = new ArrayList(); npcpickups = new ArrayList(); decals = new ArrayList(); interactors = new ArrayList(); bounded_interactors = new ArrayList(); players = new ArrayList(); triggers = new ArrayList(); } /** * More specific constructor with offset/scale values indicated */ LevelLayer(Level p, float w, float h, float ox, float oy, float sx, float sy) { this(p,w,h); xTranslate = ox; yTranslate = oy; xScale = sx; yScale = sy; if(sx!=1) { width /= sx; width -= screenWidth; } if(sy!=1) { height /= sy; } nonstandard = (xScale!=1 || yScale!=1 || xTranslate!=0 || yTranslate!=0); } /** * Get the level this layer exists in */ Level getLevel() { return parent; } // used for statistics int getActorCount() { return players.size() + bounded_interactors.size() + interactors.size() + pickups.size(); } /** * map a "normal" coordinate to this level's * coordinate system. */ float[] mapCoordinate(float x, float y) { float vx = (x + xTranslate)*xScale, vy = (y + yTranslate)*yScale; return new float[]{vx, vy}; } /** * map a screen coordinate to its layer coordinate equivalent. */ float[] mapCoordinateFromScreen(float x, float y) { float mx = map(x/xScale, 0,viewbox.w, viewbox.x,viewbox.x + viewbox.w); float my = map(y/yScale, 0,viewbox.h, viewbox.y,viewbox.y + viewbox.h); return new float[]{mx, my}; } /** * map a layer coordinate to its screen coordinate equivalent. */ float[] mapCoordinateToScreen(float x, float y) { float mx = (x/xScale - xTranslate); float my = (y/yScale - yTranslate); mx *= xScale; my *= yScale; return new float[]{mx, my}; } /** * get the mouse pointer, as relative coordinates, * relative to the indicated x/y coordinate in the layer. */ float[] getMouseInformation(float x, float y, float mouseX, float mouseY) { float[] mapped = mapCoordinateToScreen(x, y); float ax = mapped[0], ay = mapped[1]; mapped = mapCoordinateFromScreen(mouseX, mouseY); float mx = mapped[0], my = mapped[1]; float dx = mx-ax, dy = my-ay, len = sqrt(dx*dx + dy*dy); return new float[] {dx,dy,len}; } /** * draw this level layer. */ void draw() { // get the level viewbox and tranform its // reference coordinate values. float x,y,w,h; float[] mapped = mapCoordinate(viewbox.x,viewbox.y); x = mapped[0]; y = mapped[1]; w = viewbox.w / xScale; h = viewbox.h / yScale; // save applied transforms so far pushMatrix(); // transform the layer coordinates translate(viewbox.x-x, viewbox.y-y); scale(xScale, yScale); // draw all layer components if (showBackground) { handleBackground(x,y,w,h); } else { debugfunctions_drawBackground((int)width, (int)height); } if (showBoundaries) handleBoundaries(x,y,w,h); if (showPickups) handlePickups(x,y,w,h); if (showInteractors) handleNPCs(x,y,w,h); if (showActors) handlePlayers(x,y,w,h); if (showDecals) handleDecals(x,y,w,h); if (showForeground) handleForeground(x,y,w,h); if (showTriggers) handleTriggers(x,y,w,h); // restore saved transforms popMatrix(); } /** * Background color/sprites */ void handleBackground(float x, float y, float w, float h) { if (backgroundColor != -1) { background(backgroundColor); } for(Drawable s: fixed_background) { s.draw(x,y,w,h); } } /** * Boundaries should normally not be drawn, but * a debug flag can make them get drawn anyway. */ void handleBoundaries(float x, float y, float w, float h) { // regular boundaries for(Boundary b: boundaries) { b.draw(x,y,w,h); } // bounded interactor boundaries for(BoundedInteractor b: bounded_interactors) { if(b.bounding) { b.drawBoundaries(x,y,w,h); } } } /** * Handle both player and NPC Pickups. */ void handlePickups(float x, float y, float w, float h) { // player pickups for(int i = pickups.size()-1; i>=0; i--) { Pickup p = pickups.get(i); if(p.remove) { pickups.remove(i); continue; } // boundary interference? if(p.interacting && p.inMotion && !p.onlyplayerinteraction) { for(Boundary b: boundaries) { CollisionDetection.interact(b,p); } for(BoundedInteractor o: bounded_interactors) { if(o.bounding) { for(Boundary b: o.boundaries) { CollisionDetection.interact(b,p); }}}} // player interaction? for(Player a: players) { if(!a.interacting) continue; float[] overlap = a.overlap(p); if(overlap!=null) { p.overlapOccurredWith(a); break; }} // draw pickup p.draw(x,y,w,h); } // ---- npc pickups for(int i = npcpickups.size()-1; i>=0; i--) { Pickup p = npcpickups.get(i); if(p.remove) { npcpickups.remove(i); continue; } // boundary interference? if(p.interacting && p.inMotion && !p.onlyplayerinteraction) { for(Boundary b: boundaries) { CollisionDetection.interact(b,p); } for(BoundedInteractor o: bounded_interactors) { if(o.bounding) { for(Boundary b: o.boundaries) { CollisionDetection.interact(b,p); }}}} // npc interaction? for(Interactor a: interactors) { if(!a.interacting) continue; float[] overlap = a.overlap(p); if(overlap!=null) { p.overlapOccurredWith(a); break; }} // draw pickup p.draw(x,y,w,h); } } /** * Handle both regular and bounded NPCs */ void handleNPCs(float x, float y, float w, float h) { handleNPCs(x, y, w, h, interactors); handleNPCs(x, y, w, h, bounded_interactors); } /** * helper function to prevent code duplication */ void handleNPCs(float x, float y, float w, float h, ArrayList interactors) { for(int i = 0; i=0; i--) { Player a = players.get(i); if(a.remove) { players.remove(i); continue; } if(a.interacting) { // boundary interference? if(a.inMotion) { for(Boundary b: boundaries) { CollisionDetection.interact(b,a); } // boundary interference from bounded interactors? for(BoundedInteractor o: bounded_interactors) { if(o.bounding) { for(Boundary b: o.boundaries) { CollisionDetection.interact(b,a); }}}} // collisions with other sprites? if(!a.isDisabled()) { handleActorCollision(x,y,w,h,a,interactors); handleActorCollision(x,y,w,h,a,bounded_interactors); } // has the player tripped any triggers? for(int j = triggers.size()-1; j>=0; j--) { Trigger t = triggers.get(j); if(t.remove) { triggers.remove(t); continue; } float[] overlap = t.overlap(a); if(overlap==null && t.disabled) { t.enable(); } else if(overlap!=null && !t.disabled) { t.run(this, a, overlap); } } } // draw actor a.draw(x,y,w,h); } } /** * helper function to prevent code duplication */ void handleActorCollision(float x, float y, float w, float h, Actor a, ArrayList interactors) { for(int i = 0; i=0; i--) { Decal d = decals.get(i); if(d.remove) { decals.remove(i); continue; } d.draw(x,y,w,h); } } /** * Draw all foreground sprites */ void handleForeground(float x, float y, float w, float h) { for(Drawable s: fixed_foreground) { s.draw(x,y,w,h); } } /** * Triggers should normally not be drawn, but * a debug flag can make them get drawn anyway. */ void handleTriggers(float x, float y, float w, float h) { for(Drawable t: triggers) { t.draw(x,y,w,h); } } /** * passthrough events */ void keyPressed(char key, int keyCode) { for(Player a: players) { a.keyPressed(key,keyCode); }} void keyReleased(char key, int keyCode) { for(Player a: players) { a.keyReleased(key,keyCode); }} void mouseMoved(int mx, int my) { for(Player a: players) { a.mouseMoved(mx,my); }} void mousePressed(int mx, int my, int button) { for(Player a: players) { a.mousePressed(mx,my,button); }} void mouseDragged(int mx, int my, int button) { for(Player a: players) { a.mouseDragged(mx,my,button); }} void mouseReleased(int mx, int my, int button) { for(Player a: players) { a.mouseReleased(mx,my,button); }} void mouseClicked(int mx, int my, int button) { for(Player a: players) { a.mouseClicked(mx,my,button); }} } /** * Pickups! * These are special type of objects that disappear * when a player touches them and then make something * happen (like change scores or powers, etc). */ class Pickup extends Actor { String pickup_sprite = ""; int rows = 0; int columns = 0; /** * Pickups are essentially Actors that mostly do nothing, * until a player character runs into them. Then *poof*. */ Pickup(String name, String spr, int r, int c, float x, float y, boolean _persistent) { super(name); pickup_sprite = spr; rows = r; columns = c; setupStates(); setPosition(x,y); persistent = _persistent; alignSprite(CENTER,CENTER); } /** * Pickup sprite animation. */ void setupStates() { State pickup = new State(name, pickup_sprite, rows, columns); pickup.sprite.setAnimationSpeed(0.25); addState(pickup); } // wrapper void alignSprite(int halign, int valign) { active.sprite.align(halign, valign); } /** * A pickup disappears when touched by a player actor. */ void overlapOccurredWith(Actor other) { removeActor(); other.pickedUp(this); pickedUp(other); } /** * Can this object be drawn in this viewbox? */ boolean drawableFor(float vx, float vy, float vw, float vh) { boolean drawable = (vx-vw <= x && x <= vx+2*vw && vy-vh <= y && y <=vy+2*vh); if(!persistent && !drawable) { removeActor(); } return drawable; } // unused final void handleInput() {} // unused final void handleStateFinished(State which) {} // unused final void pickedUp(Pickup pickup) {} // unused, but we can overwrite it void pickedUp(Actor by) {} } /** * Players are player-controllable actors. */ abstract class Player extends Actor { // simple constructor Player(String name) { super(name); } // full constructor Player(String name, float dampening_x, float dampening_y) { super(name, dampening_x, dampening_y); } } /** * This is a helper class for Positionables, * used for recording "previous" frame data * in case we need to roll back, or do something * that requires multi-frame information */ class Position { /** * A monitoring object for informing JavaScript * about the current state of this Positionable. */ boolean monitoredByJavaScript = false; void setMonitoredByJavaScript(boolean monitored) { monitoredByJavaScript = monitored; } // ============== // variables // ============== // dimensions and positioning float x=0, y=0, width=0, height=0; // mirroring boolean hflip = false; // draw horizontall flipped? boolean vflip = false; // draw vertically flipped? // transforms float ox=0, oy=0; // offset in world coordinates float sx=1, sy=1; // scale factor float r=0; // rotation (in radians) // impulse "vector" float ix=0, iy=0; // impulse factor per frame (acts as accelator/dampener) float ixF=1, iyF=1; // external force "vector" float fx=0, fy=0; // external acceleration "vector" float ixA=0, iyA=0; int aFrameCount=0; // which direction is this positionable facing, // based on its movement in the last frame? // -1 means "not set", 0-2*PI indicates the direction // in radians (0 is ->, values run clockwise) float direction = -1; // administrative boolean animated = true; // does this object move? boolean visible = true; // do we draw this object? // ======================== // quasi-copy-constructor // ======================== void copyFrom(Position other) { x = other.x; y = other.y; width = other.width; height = other.height; hflip = other.hflip; vflip = other.vflip; ox = other.ox; oy = other.oy; sx = other.sx; sy = other.sy; r = other.r; ix = other.ix; iy = other.iy; ixF = other.ixF; iyF = other.iyF; fx = other.fx; fy = other.fy; ixA = other.ixA; iyA = other.iyA; aFrameCount = other.aFrameCount; direction = other.direction; animated = other.animated; visible = other.visible; } // ============== // methods // ============== /** * Get this positionable's bounding box */ float[] getBoundingBox() { return new float[]{x+ox-width/2, y-oy-height/2, // top-left x+ox+width/2, y-oy-height/2, // top-right x+ox+width/2, y-oy+height/2, // bottom-right x+ox-width/2, y-oy+height/2}; // bottom-left } /** * Primitive sprite overlap test: bounding box * overlap using midpoint distance. */ float[] overlap(Position other) { float w=width, h=height, ow=other.width, oh=other.height; float[] bounds = getBoundingBox(); float[] obounds = other.getBoundingBox(); if(bounds==null || obounds==null) return null; float xmid1 = (bounds[0] + bounds[2])/2; float ymid1 = (bounds[1] + bounds[5])/2; float xmid2 = (obounds[0] + obounds[2])/2; float ymid2 = (obounds[1] + obounds[5])/2; float dx = xmid2 - xmid1; float dy = ymid2 - ymid1; float dw = (w + ow)/2; float dh = (h + oh)/2; // no overlap if the midpoint distance is greater // than the dimension half-distances put together. if(abs(dx) > dw || abs(dy) > dh) { return null; } // overlap float angle = atan2(dy,dx); if(angle<0) { angle += 2*PI; } float safedx = dw-dx, safedy = dh-dy; return new float[]{dx, dy, angle, safedx, safedy}; } /** * Apply all the transforms to the * world coordinate system prior to * drawing the associated Positionable * object. */ void applyTransforms() { // we need to make sure we end our transforms // in such a way that integer coordinates lie // on top of canvas grid coordinates. Hence // all the (int) casting in translations. translate((int)x, (int)y); if (r != 0) { rotate(r); } if(hflip) { scale(-1,1); } if(vflip) { scale(1,-1); } scale(sx,sy); translate((int)ox, (int)oy); } /** * Good old toString() */ String toString() { return "position: "+x+"/"+y+ ", impulse: "+ix+"/"+iy+ " (impulse factor: "+ixF+"/"+iyF+")" + ", forces: "+fx+"/"+fy+ " (force factor: "+ixA+"/"+iyA+")"+ ", offset: "+ox+"/"+oy; } } /** * Manipulable object: translate, rotate, scale, flip h/v */ abstract class Positionable extends Position implements Drawable { // HELPER FUNCTION FOR JAVASCRIPT void jsupdate() { if(monitoredByJavaScript && javascript != null) { javascript.updatedPositionable(this); }} /** * We track two frames for computational purposes, * such as performing boundary collision detection. */ Position previous = new Position(); /** * Boundaries this positionable is attached to. */ ArrayList boundaries; /** * Decals that are drawn along with this positionable, * but do not contribute to any overlap or collision * detection, nor explicitly interact with things. */ ArrayList decals; // shortcut variable that tells us whether // or not this positionable needs to perform // boundary collision checks boolean inMotion = false; /** * Cheap constructor */ Positionable() { boundaries = new ArrayList(); decals = new ArrayList(); } /** * Set up a manipulable object */ Positionable(float _x, float _y, float _width, float _height) { this(); x = _x; y = _y; width = _width; height = _height; ox = width/2; oy = -height/2; } /** * change the position, absolute */ void setPosition(float _x, float _y) { x = _x; y = _y; previous.x = x; previous.y = y; aFrameCount = 0; direction = -1; jsupdate(); } /** * Attach this positionable to a boundary. */ void attachTo(Boundary b) { boundaries.add(b); } /** * Check whether this positionable is * attached to a specific boundary. */ boolean isAttachedTo(Boundary b) { return boundaries.contains(b); } /** * Detach this positionable from a * specific boundary. */ void detachFrom(Boundary b) { boundaries.remove(b); } /** * Detach this positionable from all * boundaries that it is attached to. */ void detachFromAll() { boundaries.clear(); } /** * attach a Decal to this positionable. */ void addDecal(Decal d) { decals.add(d); d.setOwner(this); } /** * detach a Decal from this positionable. */ void removeDecal(Decal d) { decals.remove(d); } /** * detach all Decal from this positionable. */ void removeAllDecals() { decals.clear(); } /** * change the position, relative */ void moveBy(float _x, float _y) { x += _x; y += _y; previous.x = x; previous.y = y; aFrameCount = 0; jsupdate(); } /** * check whether this Positionable is moving. If it's not, * it will not be boundary-collision-evalutated. */ void verifyInMotion() { inMotion = (ix!=0 || iy!=0 || fx!=0 || fy!=0 || ixA!=0 || iyA !=0); } /** * set the impulse for this object */ void setImpulse(float x, float y) { ix = x; iy = y; jsupdate(); verifyInMotion(); } /** * set the impulse coefficient for this object */ void setImpulseCoefficients(float fx, float fy) { ixF = fx; iyF = fy; jsupdate(); verifyInMotion(); } /** * add to the impulse for this object */ void addImpulse(float _ix, float _iy) { ix += _ix; iy += _iy; jsupdate(); verifyInMotion(); } /** * Update which direction this positionable is * "looking at". */ void setViewDirection(float dx, float dy) { if(dx!=0 || dy!=0) { direction = atan2(dy,dx); if(direction<0) { direction+=2*PI; }} } /** * collisions may force us to stop object's movement. */ void stop() { ix = 0; iy = 0; jsupdate(); } /** * Set the external forces acting on this actor */ void setForces(float _fx, float _fy) { fx = _fx; fy = _fy; jsupdate(); verifyInMotion(); } /** * Augment the external forces acting on this actor */ void addForces(float _fx, float _fy) { fx += _fx; fy += _fy; jsupdate(); verifyInMotion(); } /** * set the uniform acceleration for this object */ void setAcceleration(float ax, float ay) { ixA = ax; iyA = ay; aFrameCount = 0; jsupdate(); verifyInMotion(); } /** * Augment the accelleration for this object */ void addAccelleration(float ax, float ay) { ixA += ax; iyA += ay; jsupdate(); verifyInMotion(); } /** * set the translation to be the specified x/y values. */ void setTranslation(float x, float y) { ox = x; oy = y; jsupdate(); } /** * set the scale to uniformly be the specified value. */ void setScale(float s) { sx = s; sy = s; jsupdate(); } /** * set the scale to be the specified x/y values. */ void setScale(float x, float y) { sx = x; sy = y; jsupdate(); } /** * set the rotation to be the specified value. */ void setRotation(float _r) { r = _r % (2*PI); jsupdate(); } /** * flip this object horizontally. */ void setHorizontalFlip(boolean _hflip) { if(hflip!=_hflip) { ox = -ox; } for(Decal d: decals) { d.setHorizontalFlip(_hflip); } hflip = _hflip; jsupdate(); } /** * flip this object vertically. */ void setVerticalFlip(boolean _vflip) { if(vflip!=_vflip) { oy = -oy; } for(Decal d: decals) { d.setVerticalFlip(_vflip); } vflip = _vflip; jsupdate(); } /** * set this object's visibility */ void setVisibility(boolean _visible) { visible = _visible; jsupdate(); } /** * mark object static or animated */ void setAnimated(boolean _animated) { animated = _animated; jsupdate(); } /** * get the previous x coordinate. */ float getPrevX() { return previous.x + previous.ox; } /** * get the previous y coordinate. */ float getPrevY() { return previous.y + previous.oy; } /** * get the current x coordinate. */ float getX() { return x + ox; } /** * get the current y coordinate. */ float getY() { return y + oy; } /** * Set up the coordinate transformations * and then call whatever implementation * of "drawObject" exists. */ void draw(float vx, float vy, float vw, float vh) { // Draw, if visible if (visible && drawableFor(vx,vy,vw,vh)) { pushMatrix(); applyTransforms(); drawObject(); for(Decal d: decals) { d.draw(); } popMatrix(); } // Update position for next the frame, // based on impulse and force. if(animated) { update(); } } /** * must be implemented by subclasses, * to indicate whether this object is * visible in this viewbox. */ abstract boolean drawableFor(float vx, float vy, float vw, float vh); /** * Update all the position parameters. * If fixed is not null, it is the boundary * we just attached to, and we cannot detach * from it on the same frame. */ void update() { // cache frame information previous.copyFrom(this); // work external forces into our current impulse addImpulse(fx,fy); // work in impulse coefficients (typically, drag) ix *= ixF; iy *= iyF; // not on a boundary: unrestricted motion, // so make sure the acceleration factor exists. if(boundaries.size()==0) { aFrameCount++; } // we're attached to one or more boundaries, so we // are subject to (compound) impulse redirection. else { aFrameCount = 0; float[] redirected = new float[]{ix, iy}; for(int b=boundaries.size()-1; b>=0; b--) { Boundary boundary = boundaries.get(b); if(!boundary.disabled) { redirected = boundary.redirectForce(this, redirected[0], redirected[1]); } if(boundary.disabled || !boundary.supports(this)) { detachFrom(boundary); continue; } } ix = redirected[0]; iy = redirected[1]; } // Not unimportant: cutoff resolution. if(abs(ix) < 0.01) { ix = 0; } if(abs(iy) < 0.01) { iy = 0; } // update the physical position x += ix + (aFrameCount * ixA); y += iy + (aFrameCount * iyA); } /** * Reset this positional to its previous state */ void rewind() { copyFrom(previous); jsupdate(); } // implemented by subclasses abstract void drawObject(); // mostly for debugging purposes String toString() { return width+"/"+height + "\n" + "current: " + super.toString() + "\n" + "previous: " + previous.toString() + "\n"; } } /** * Every thing in 2D sprite games happens in "Screen"s. * Some screens are menus, some screens are levels, but * the most generic class is the Screen class */ abstract class Screen { // is this screen locked, or can it be swapped out? boolean swappable = false; // level dimensions float width, height; /** * simple Constructor */ Screen(float _width, float _height) { width = _width; height = _height; } /** * allow swapping for this screen */ void setSwappable() { swappable = true; } /** * draw the screen */ abstract void draw(); /** * perform any cleanup when this screen is swapped out */ abstract void cleanUp(); /** * passthrough events */ abstract void keyPressed(char key, int keyCode); abstract void keyReleased(char key, int keyCode); abstract void mouseMoved(int mx, int my); abstract void mousePressed(int mx, int my, int button); abstract void mouseDragged(int mx, int my, int button); abstract void mouseReleased(int mx, int my, int button); abstract void mouseClicked(int mx, int my, int button); } /** * A generic, abstract shape class. * This class has room to fit anything * up to to cubic Bezier curves, with * additional parameters for x/y scaling * at start and end points, as well as * rotation at start and end points. */ abstract class ShapePrimitive { String type = "unknown"; // coordinate values float x1=0, y1=0, cx1=0, cy1=0, cx2=0, cy2=0, x2=0, y2=0; // transforms at the end points float sx1=1, sy1=1, sx2=1, sy2=1, r1=0, r2=0; // must be implemented by extensions abstract void draw(); // set the scale values for start and end points void setScales(float _sx1, float _sy1, float _sx2, float _sy2) { sx1=_sx1; sy1=_sy1; sx2=_sx2; sy2=_sy2; } // set the rotation at start and end points void setRotations(float _r1, float _r2) { r1=_r1; r2=_r2; } // generate a string representation of this shape. String toString() { return type+" "+x1+","+y1+","+cx1+","+cy1+","+cx2+","+cy2+","+x2+","+y2+ " - "+sx1+","+sy1+","+sx2+","+sy2+","+r1+","+r2; } } /** * This class models a dual-purpose 2D * point, acting either as linear point * or as tangental curve point. */ class Point extends ShapePrimitive { // will this behave as curve point? boolean cpoint = false; // since points have no "start and end", alias the values float x, y, cx, cy; Point(float x, float y) { type = "Point"; this.x=x; this.y=y; this.x1=x; this.y1=y; } // If we know the next point, we can determine // the rotation for the sprite at this point. void setNext(float nx, float ny) { if (!cpoint) { r1 = atan2(ny-y,nx-x); } } // Set the curve control values, and turn this // into a curve point (even if it already was) void setControls(float cx, float cy) { cpoint = true; this.cx=(cx-x); this.cy=(cy-y); this.cx1=this.cx; this.cy1=this.cy; r1 = PI + atan2(y-cy,x-cx); } // Set the rotation for the sprite at this point void setRotation(float _r) { r1=_r; } void draw() { // if curve, show to-previous control point if (cpoint) { line(x-cx,y-cy,x,y); ellipse(x-cx,y-cy,3,3); } point(x,y); // if curve, show to-next control point 2 if (cpoint) { line(x,y,x+cx,y+cy); ellipse(x+cx,y+cy,3,3); } } // this method gets called during edit mode for setting scale and/or rotation boolean over(float mx, float my, int boundary) { boolean mainpoint = (abs(x-mx) < boundary && abs(y-my) < boundary); return mainpoint || overControl(mx, my, boundary); } // this method gets called during edit mode for setting rotation boolean overControl(float mx, float my, int boundary) { return (abs(x+cx-mx) < boundary && abs(y+cy-my) < boundary); } } /** * Generic line class */ class Line extends ShapePrimitive { // Vanilla constructor Line(float x1, float y1, float x2, float y2) { type = "Line"; this.x1=x1; this.y1=y1; this.x2=x2; this.y2=y2; } // Vanilla draw method void draw() { line(x1,y1,x2,y2); } } /** * Generic cubic Bezier curve class */ class Curve extends ShapePrimitive { // Vanilla constructor Curve(float x1, float y1, float cx1, float cy1, float cx2, float cy2, float x2, float y2) { type = "Curve"; this.x1=x1; this.y1=y1; this.cx1=cx1; this.cy1=cy1; this.cx2=cx2; this.cy2=cy2; this.x2=x2; this.y2=y2; } // Vanilla draw method void draw() { bezier(x1,y1,cx1,cy1,cx2,cy2,x2,y2); } } /** * The SoundManager is a static class that is responsible * for handling audio loading and playing. Any audio * instructions are delegated to this class. In Processing * this uses the Minim library, and in Processing.js it * uses the HTML5