Tower Defense: A few concept pointers needed

Hi, been developing a Tower Defense-game in JME3 over the past weeks, getting pretty far and are at the point where its all coming together. However, I think I may need a pointer on the physics-part. The setup is this (so far)



A world (a TerrainQuad, w. roads, mountains etc., made physical)

A base (a Box-shape, with an added GhostControl, to check for incoming enemies, made physical to stop it from falling through the terrain)

A player (a CharacterControl, able to walk around the map, made physical to stop it from falling through terrain)

Towers (a Box-shape, with an added GhostControl, to check for incoming enemies - fires CannonBalls at enemies when they are in range, made physical to stop it from falling through terrain - and to stop enemies walking through them). Using picking to place them on the terrain.

Enemies (CharacterControl, using setWalkDirection in each update loop, to find the base (no path-finding-algorithm implemented yet. ), made physical to stop them falling through terrain and from walking through towers)



NB: I like the tutorials, and the fact that they encourage the reader to figure out the solution themselves - so I dont need an entire solution - just hints of where to look etc.



Q1: What parts of the above setup would need to be physical in a simplistic setting (right now, its just about getting the game-framework going, eg, A base, mobs reach it they disappear, tower who shoot at mobs, mobs disappear if health = 0, mobs walk towards base etc.)?



It seems as though my base-ghost-control detects a mob in range, and tries to remove it:



[java]public void collision(PhysicsCollisionEvent event) {

if (event.getNodeA().getName().startsWith("OTO") && event.getNodeB().equals(this.spatial)) {

Node n = (Node) event.getNodeA();

MobControl c = n.getControl(MobControl.class);

app.getSysManager().getBulletAppState().getPhysicsSpace().remove(c);

n.removeFromParent();

} else if (event.getNodeB().getName().startsWith("OTO") && event.getNodeA().equals(this.spatial)) {

Spatial n = event.getNodeB();

MobControl c = n.getControl(MobControl.class);

app.getSysManager().getBulletAppState().getPhysicsSpace().remove(c);

n.removeFromParent();

}

}[/java]



which removes the first mob (not immediately, but it removes it) - the second enemy takes a bit longer to get removed and from then on, no enemies are removed from the scene.



Q2: Is this due to poor programming on my part or am I missing something ?



I’m using a TimerTask (implementing Callable) to create mobs (through application.enqueue(Callable c)) - which seems to work fine - although, I’d like to be able to spawn enemies at a semi-random intervals (so it wouldn’t be every 1 seconds, it would be @ 0, then @ 0.3, then @ 1.4 …



Q3: Any api that would let me achieve this semi-randomg spawning ?



I’m also using TimerTask to fire at enemies (when they collide with the TowerGhostControl, a TimerTask is created and scheduled every second) - then when they exit the ScheduledFuture is cancelled. Works, but not quite how I’d like it to. Right now, the projectiles themselves are physical - which could cause a problem as I’d like the endgame scenario to include a large number of towers and enemies.



Q4: Any hints on how to implement a better firing?



I read that Normen recommended the following to another tower-defenes-programmer:


You’d have Spatials for the enemies and wall parts etc, use picking to check for collisions with the terrain and other objects, use simple location info to determine where to shoot etc etc.. I suggest you do the tutorials and read the primers in the wiki (basically work your way through the first part of this page). After doing that, most of the questions on how to do that should be easy to solve and you will also have learned a bag of tricks that will definitely come handy in the later development stages of your game.


Q5: am I misreading something in the quote or did I actually implement it somewhat how he described?

Kindest regards,
Asser

I got a better shooting implemented, although its till too heavy on the physics. First tower looks and feels smooth, two towers feel a bit choppy, and 3+ is unplayable. I must be doing something wrong, since I can see in the jMETests-project that the BrickTower + CannonBall-tests has 100+ bricks + cannonballs and it runs smooth as silk.



I’m looking for a tower-shooting-logic, that could handle 100+ towers each shooting 5 times per second, which would allow me to check if the projectile hit anything.



Any hints ?



I got my TowerGhostControl here:



[java]

/*

  • To change this template, choose Tools | Templates
  • and open the template in the editor.

    */

    package dabble.entity.structure.tower;



    import com.jme3.asset.TextureKey;

    import com.jme3.bullet.collision.PhysicsCollisionEvent;

    import com.jme3.bullet.collision.PhysicsCollisionGroupListener;

    import com.jme3.bullet.collision.PhysicsCollisionListener;

    import com.jme3.bullet.collision.PhysicsCollisionObject;

    import com.jme3.bullet.collision.shapes.BoxCollisionShape;

    import com.jme3.bullet.collision.shapes.SphereCollisionShape;

    import com.jme3.bullet.control.GhostControl;

    import com.jme3.bullet.control.RigidBodyControl;

    import com.jme3.material.Material;

    import com.jme3.math.Vector3f;

    import com.jme3.scene.Geometry;

    import com.jme3.scene.Node;

    import com.jme3.scene.Spatial;

    import com.jme3.scene.shape.Sphere;

    import com.jme3.scene.shape.Sphere.TextureMode;

    import com.jme3.texture.Texture;

    import dabble.DabbleGame;

    import dabble.DabbleStatic;

    import dabble.entity.ProjectileControl;

    import dabble.entity.ProjectileEnum;

    import dabble.entity.Shoot;

    import dabble.entity.mob.control.MobControl;

    import java.util.concurrent.ScheduledFuture;

    import java.util.concurrent.TimeUnit;



    /**
  • Allows for detection of mobs entering vicinity of tower
  • @author Asser

    /

    public class TowerGhostControl extends GhostControl implements PhysicsCollisionGroupListener, PhysicsCollisionListener {



    DabbleGame app;

    Spatial currentTarget;

    boolean hasTarget;

    Shoot shoot;

    TowerEnum tower;

    ScheduledFuture<Shoot> future;

    Vector3f origin, target;

    Node hostile;

    String towerName;

    float range;

    private float sps;

    private long lastShot;

    private float dist;

    private Material projectileMat;

    private TextureKey key2;

    private Texture tex2;

    private Geometry projectileGeo;

    private RigidBodyControl projectilePhys;

    private ProjectileControl projectileCon;

    private static final Sphere projectileShape;



    static {

    /
    * Initialize the cannon ball geometry /

    projectileShape = new Sphere(8, 8, 0.2f, true, false);

    projectileShape.setTextureMode(TextureMode.Projected);

    }



    TowerGhostControl(BoxCollisionShape boxCollisionShape, DabbleGame _app, TowerEnum tower) {

    super(boxCollisionShape);

    this.app = _app;

    this.init();

    this.tower = tower;





    }



    TowerGhostControl(SphereCollisionShape sphereCollisionShape, DabbleGame _app, TowerEnum tower) {

    super(sphereCollisionShape);

    this.app = _app;

    this.init();

    this.tower = tower;

    }



    public void collision(PhysicsCollisionEvent event) {

    //TODO: Implement check for all mob-names

    //if ((event.getNodeA().getName().startsWith("OTO") && !event.getNodeB().getName().startsWith("My Terrain"))

    // || (event.getNodeB().getName().startsWith("OTO") && !event.getNodeA().getName().startsWith("My Terrain"))) {

    // System.out.println(this.spatial.getName() + " == " + event.getNodeA().getName() + " == " + event.getNodeB().getName());

    //}

    //(should be a very fast calculation, since this collision() is called upon every collision in the game)

    /


    if ((event.getNodeA().getName().startsWith("OTO") && event.getNodeB().equals(this.spatial))

    || (event.getNodeB().getName().startsWith("OTO") && event.getNodeA().equals(this.spatial))) {

    hostile = getHostile(event);

    target = hostile.getWorldTranslation();

    //TOOD: Handle detection of mobs gracefully

    if (!hasTarget) {

    //Found target, YAY!

    hasTarget = true;

    currentTarget = event.getNodeA();

    origin = this.getPhysicsLocation();

    //Now kill it!

    shoot = new Shoot(app, ProjectileEnum.BULLET1, origin, target);



    future = (ScheduledFuture<Shoot>) app.getSysManager().getExecutor().scheduleAtFixedRate(shoot, 0, 2000, TimeUnit.MILLISECONDS);

    }

    range = Float.valueOf(this.spatial.getUserData(DabbleStatic.userDataRange).toString());

    float dist = origin.distance(target);

    if (hasTarget && dist > range) {

    future.cancel(true);

    hasTarget = false;

    }

    }



    /

    }



    private void init() {

    //this.setCollideWithGroups(PhysicsCollisionObject.COLLISION_GROUP_02);

    //^^needs further testing :slight_smile:

    this.app.getSysManager().getBulletAppState().getPhysicsSpace().addCollisionGroupListener(this, PhysicsCollisionObject.COLLISION_GROUP_02);

    this.app.getSysManager().getBulletAppState().getPhysicsSpace().addCollisionListener(this);

    //this.tower = this.spatial.getUserData(DabbleStatic.userDataTowerType);



    //towerName =



    hasTarget = false;

    }



    public boolean collide(PhysicsCollisionObject nodeA, PhysicsCollisionObject nodeB) {



    return true;

    }



    private Node getHostile(PhysicsCollisionEvent event) {

    return (Node) (event.getNodeA().getName().startsWith("OTO") ? event.getNodeA() : event.getNodeB());

    }



    @Override

    public void update(float tpf) {



    range = (Float) this.spatial.getUserData(DabbleStatic.userDataRange);

    sps = (Integer) this.spatial.getUserData(DabbleStatic.userDataspsStr);



    if (currentTarget != null) {

    dist = this.spatial.getWorldTranslation().distance(currentTarget.getWorldTranslation());



    if (dist > range) {

    currentTarget = null;

    dist = 0;

    }

    }





    //No current target, or it slipped away - find new (first available):

    if (currentTarget == null) {

    for (PhysicsCollisionObject i : this.getOverlappingObjects()) {

    if (i instanceof MobControl) {

    //TODO: Add i to collection of reachable mobs - pick one using AI according to level of intelligence

    //Object userObject = i.getUserObject();

    MobControl m = (MobControl) i;

    currentTarget = m.getSpatial();

    break;

    }

    }

    }



    if (currentTarget != null) {

    if (lastShot == 0f) //First shot

    {

    lastShot = System.currentTimeMillis();

    //System.out.println("Fired at: " + currentTarget);

    fire(currentTarget.getWorldTranslation(), ProjectileEnum.BULLET1);



    } else if (lastShot + (1000l / (long) sps) < System.currentTimeMillis()) //Shoot again

    {

    lastShot = System.currentTimeMillis();

    fire(currentTarget.getWorldTranslation(), ProjectileEnum.BULLET1);

    //System.out.println("Fired at: " + currentTarget);

    }

    }



    super.update(tpf);

    }



    private void fire(Vector3f target, ProjectileEnum projType) {

    // Add model for projectile

    projectileMat = new Material(app.getAssetManager(), projType.getModel());

    key2 = new TextureKey(projType.getTexture());

    key2.setGenerateMips(true);

    tex2 = app.getAssetManager().loadTexture(key2);

    projectileMat.setTexture("ColorMap", tex2);



    /
    Create a projectile geometry and attach to scene graph. /

    projectileGeo = new Geometry(projType.getName(), projectileShape);

    projectileGeo.setMaterial(projectileMat);

    app.getRootNode().attachChild(projectileGeo);



    /
    * Position the cannon ball /

    //ball_geo.setLocalTranslation(cam/.getLocation());

    projectileGeo.setLocalTranslation(this.getPhysicsLocation().addLocal(0, this.tower.getSize().y+0.5f, 0));

    // ball_geo.setLocalTranslation(player.getWalkDirection().multLocal(-2f));



    /
    * Make the ball physcial with a mass > 0.0f /

    projectilePhys = new RigidBodyControl(projType.getWeight());





    /
    * Add physical ball to physics space. /

    projectileGeo.addControl(projectilePhys);



    projectileCon = new ProjectileControl(projectileGeo, target, app);

    projectileGeo.addControl(projectileCon);



    app.getSysManager().getBulletAppState().getPhysicsSpace().add(projectilePhys);



    /
    * Accelerate the physical ball to shoot it. /

    //ball_phy.setLinearVelocity(cam.getDirection().mult(25));

    Vector3f shootDir = target.subtract(this.getPhysicsLocation());

    shootDir.normalizeLocal().multLocal(25); //TODO: Incorporate walking speed



    projectilePhys.setLinearVelocity(shootDir);

    }

    }

    [/java]



    And my TowerEnum (w. the Create-method):



    [java]

    /

  • Contains the logic to create towers, with their default settings etc.

    */

    package dabble.entity.structure.tower;



    import com.jme3.bullet.collision.shapes.BoxCollisionShape;

    import com.jme3.bullet.collision.shapes.SphereCollisionShape;

    import com.jme3.bullet.control.RigidBodyControl;

    import com.jme3.material.Material;

    import com.jme3.math.ColorRGBA;

    import com.jme3.math.Vector3f;

    import com.jme3.scene.Geometry;

    import com.jme3.scene.shape.Box;

    import dabble.DabbleGame;

    import dabble.DabbleStatic;

    import dabble.entity.mob.MobEnum;



    /**

    *
  • @author Asser

    /

    public enum TowerEnum {



    //TODO: Get settings from centralized class! :slight_smile:

    ARROW (1,10,2,3,15,2,"",1f, ColorRGBA.Blue,"Common/MatDefs/Misc/Unshaded.j3md", "Arrow", 25f, new Vector3f(0.5f, 0.5f, 0.5f)),

    CANNON (1,10,2,3,15,2,"",1f,ColorRGBA.Cyan,"Common/MatDefs/Misc/Unshaded.j3md", "Cannon",50f,new Vector3f(0.5f, 0.5f, 0.5f)),

    ICE (1,10,2,3,15,2,"",1f,ColorRGBA.Green,"Common/MatDefs/Misc/Unshaded.j3md", "Ice",50f,new Vector3f(0.5f, 0.5f, 0.5f)),

    FIRE (1,10,2,3,15,2,"",1f,ColorRGBA.Magenta,"Common/MatDefs/Misc/Unshaded.j3md", "Fire",50f,new Vector3f(0.5f, 0.5f, 0.5f)),

    BARRACKS (1,10,2,3,15,2,"",1f,ColorRGBA.Orange,"Common/MatDefs/Misc/Unshaded.j3md", "Barracks",50f,new Vector3f(0.5f, 0.5f, 0.5f)),

    WIZARD (1,10,2,3,15,2,"",1f,ColorRGBA.Red,"Common/MatDefs/Misc/Unshaded.j3md", "Wizard",50f,new Vector3f(0.5f, 0.5f, 0.5f));



    private final int sps;

    private final int clip;

    private final int meleeDmg;

    private final int rangedDmg;

    private final int hp;

    private final Vector3f size;



    public String getMat() {

    return mat;

    }



    public String getName() {

    return name;

    }



    public float getRange() {

    return range;

    }

    private final int reload;

    private final String model;

    private final float scale;

    private final ColorRGBA color;

    private final String mat;

    private final String name;

    private final float range;



    public ColorRGBA getColor() {

    return color;

    }



    private TowerEnum(int sps, int clip, int meleeDmg, int rangedDmg, int hp,

    int reload, String model, float scale, ColorRGBA color, String mat,

    String name, float range, Vector3f size) {

    this.range = range;

    this.sps = sps;

    this.clip = clip;

    this.meleeDmg = meleeDmg;

    this.rangedDmg = rangedDmg;

    this.hp = hp;

    this.reload = reload;

    this.model = model;

    this.scale = scale;

    this.color = color;

    this.mat = mat;

    this.name = name;

    this.size = size;

    }



    public int getMeleeDmg() {

    return meleeDmg;

    }



    public String getModel() {

    return model;

    }



    public int getRangedDmg() {

    return rangedDmg;

    }



    public float getScale() {

    return scale;

    }



    public int getClip() {

    return clip;

    }



    public int getDmg() {

    return meleeDmg;

    }



    public int getHp() {

    return hp;

    }



    public int getReload() {

    return reload;

    }



    public int getSps() {

    return sps;

    }



    public void create(Vector3f pt, DabbleGame app) {



    Box tower = new Box(Vector3f.ZERO, 0.5f, 0.5f, 0.5f);



    Geometry tower_geo = new Geometry(name, tower);

    //Add material to sides of box:

    Material tower_mat = new Material(app.getAssetManager(), mat);

    tower_mat.setColor("Color", color);

    tower_geo.setMaterial(tower_mat);

    //Create tower above the point on the terrain that was clicked:

    tower_geo.setLocalTranslation(pt.addLocal(0, tower.getYExtent(), 0)); //TODO: Get settings from centralized class

    app.getRootNode().attachChild(tower_geo);

    //Make towers physical (rigidbodycontrol)

    RigidBodyControl tower_phy = new RigidBodyControl(1f);



    //set user data:–>

    tower_geo.setUserData(DabbleStatic.userDatahealthStr, hp);

    tower_geo.setUserData(DabbleStatic.userDatadmgStr, meleeDmg);

    tower_geo.setUserData(DabbleStatic.userDataclipStr, clip);

    tower_geo.setUserData(DabbleStatic.userDataspsStr, sps);

    tower_geo.setUserData(DabbleStatic.userDatareloadStr, reload);

    tower_geo.setUserData(DabbleStatic.userDataRange, range);

    tower_geo.setUserData(DabbleStatic.userdataMobNode, app.getInGameState().getMobManager().getMobNode());

    //tower_geo.setUserData(DabbleStatic.userDataTowerType, this);

    //<–



    tower_geo.addControl(tower_phy);



    TowerGhostControl ghost = new TowerGhostControl(

    new SphereCollisionShape(range),app,TowerEnum.ARROW);

    //new BoxCollisionShape(new Vector3f(1, 1, 1)), app); // a box-shaped ghost that allows detection of mobs in vicinity :slight_smile:

    //System.out.println("Tower created with range: "+range);

    tower_geo.addControl(ghost);

    //TowerControl tCon = new TowerControl(MobEnum.OTO, app);

    //tower_geo.addControl(tCon);



    app.getSysManager().getBulletAppState().getPhysicsSpace().add(tower_phy);

    app.getSysManager().getBulletAppState().getPhysicsSpace().add(ghost);

    }



    Vector3f getSize() {

    return size;

    }

    }

    [/java]



    Any my projectile control:



    [java]/

  • To change this template, choose Tools | Templates
  • and open the template in the editor.

    */

    package dabble.entity;



    import com.jme3.bullet.control.RigidBodyControl;

    import com.jme3.export.Savable;

    import com.jme3.math.Quaternion;

    import com.jme3.math.Transform;

    import com.jme3.math.Vector3f;

    import com.jme3.renderer.RenderManager;

    import com.jme3.renderer.ViewPort;

    import com.jme3.scene.Geometry;

    import com.jme3.scene.Spatial;

    import com.jme3.scene.control.AbstractControl;

    import com.jme3.scene.control.Control;

    import dabble.DabbleGame;



    /**

    *
  • @author Asser

    */

    public class ProjectileControl extends AbstractControl implements Savable, Cloneable {



    Geometry geo;

    Transform bulletTrans;

    //ProjectControl shoBuRu;

    Quaternion quaObj;

    Vector3f frontVec;

    Vector3f target;

    DabbleGame app;

    float timer2;

    float firstTime;



    public ProjectileControl(Geometry _geo, Vector3f _target, DabbleGame _app) {

    this.geo = _geo;

    this.target = _target;

    this.app = _app;



    //bulletTrans = this.spatial.getWorldTransform().setScale(0.5f, 0.5f, 0.5f);

    //geo.setLocalTransform(bulletTrans);



    //quaObj = this.spatial.getWorldRotation();

    //frontVec = quaObj.mult(Vector3f.UNIT_Z).normalize();

    }



    public Control cloneForSpatial(Spatial arg0) {

    return null;

    }



    protected void controlRender(RenderManager arg0, ViewPort arg1) {

    }



    @Override

    protected void controlUpdate(float arg0) {



    timer2 += arg0 * 4f;

    //this.geo.move(frontVec.mult(1.2f));



    if (timer2 > 3f) {

    this.geo.removeControl(this);

    this.geo.getControl(RigidBodyControl.class).destroy();

    this.geo.removeFromParent();

    //app.getRootNode().detachChild(this.geo);

    }

    }

    }

    [/java]



    Anyone able to spot any implementation-wise-flaws or just help me out a bit ?

Afaik you dont need a Geometry in your Controls, when extending AbstractControl you get access to this.spatial which is the spatial you attach the control to.

At the moment your progam is working hard … not smart… you need to think a little less linearly and you can make some big savings. The following are all examples that may or may not be the best way to do something for you - see if you can think of better ways:



For example the towers don’t move (other than spinning to aim) when fired…so why have them colliding with the terrain? Just place them in a fixed location. Check the area is clear, if it is then drop them in place.



For shooting do you really need accurate physics? In fact that can make life harder as you need to predict the targets movement to shoot in the right place. Just have the towers fire lasers/homing rockets/whatever. (Maybe some towers have physics and some not).



For route finding (I know you didn’t mention this yet) don’t try and calculate it for each enemy all the time. Instead divide the board into cells and update each cell with the direction to the exit. Each enemy just reads its local cell and knows which way to go.



Place each tower and when you do “block” that cell in the board and recalculate the route for all the others. Now enemies automatically avoid the tower and you don’t need collision detection there either. You also don’t need collision with the floor. Just move the enemies with a constant y and with x/y direction decided based on their speed and the direction stored in the cell. This also lets you check for complete blocks when placing towers…if it would “isolate” a cell then don’t allow it to be placed.



Generally tower defence enemies don’t block each other - since they tend to come in swarms. If yours do then be prepared for lots of jostling around in busy levels.



Just a few ideas :slight_smile:

1 Like

Very much appreciated, the both of you. It’s good to get some opinions and ideas.