5
\$\begingroup\$

I have walls that I can spawn. They wll move in one of four directions, up, down, right or left. Now I can set, wether the wall is thin, wide, long or short, but the problem is:

Nearly everything I do with the walls depends on wether they move up, down, left or right and because of this I use my direction enum extremely often and have to pass it the current state of the wall everytime.

import java.util.Random;

import com.AndroidTest.game.Constants;
import com.AndroidTest.game.GameWorld.GameWorld;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.physics.box2d.Body;
import com.badlogic.gdx.physics.box2d.BodyDef;
import com.badlogic.gdx.physics.box2d.BodyDef.BodyType;
import com.badlogic.gdx.physics.box2d.PolygonShape;

public class Wall {
    public enum Direction {
        UP {
            @Override
            public Vector2 getPosition(Random rand, Size size, Length length) {
                return new Vector2(rand.nextFloat()*(Constants.WORLD_WIDTH-size.value)+size.value/2, -length.valueVert);
            }

            @Override
            public float getWidth(Size size, Length length) {
                return size.value/2;
            }

            @Override
            public float getHeight(Size size, Length length) {
                return length.valueVert;
            }

            @Override
            public Vector2 getLinearVelocity(float speed) {
                return new Vector2(0f, speed);
            }

            @Override
            public boolean getDeath(Body body, Size size, Length length) {
                return (body.getPosition().y >= Constants.WORLD_HEIGHT+getHeight(size, length)) ? true : false;
            }
        },
        DOWN {
            @Override
            public Vector2 getPosition(Random rand, Size size, Length length) {
                return new Vector2(rand.nextFloat()*(Constants.WORLD_WIDTH-size.value)+size.value/2, Constants.WORLD_HEIGHT+length.valueVert);
            }

            @Override
            public float getWidth(Size size, Length length) {
                return size.value/2;
            }

            @Override
            public float getHeight(Size size, Length length) {
                return length.valueVert;
            }

            @Override
            public Vector2 getLinearVelocity(float speed) {
                return new Vector2(0f, -speed);
            }

            @Override
            public boolean getDeath(Body body, Size size, Length length) {
                return (body.getPosition().y <= -getHeight(size, length)) ? true : false;
            }
        },
        RIGHT {
            @Override
            public Vector2 getPosition(Random rand, Size size, Length length) {
                return new Vector2(-length.valueHori, rand.nextFloat()*(Constants.WORLD_HEIGHT-size.value)+size.value/2);
            }

            @Override
            public float getWidth(Size size, Length length) {
                return length.valueHori;
            }

            @Override
            public float getHeight(Size size, Length length) {
                return size.value/2;
            }

            @Override
            public Vector2 getLinearVelocity(float speed) {
                return new Vector2(speed, 0f);
            }

            @Override
            public boolean getDeath(Body body, Size size, Length length) {
                return (body.getPosition().x >= Constants.WORLD_WIDTH+getHeight(size, length)) ? true : false;
            }
        },
        LEFT {
            @Override
            public Vector2 getPosition(Random rand, Size size, Length length) {
                return new Vector2(Constants.WORLD_WIDTH+length.valueHori, rand.nextFloat()*(Constants.WORLD_HEIGHT-size.value)+size.value/2);
            }

            @Override
            public float getWidth(Size size, Length length) {
                return length.valueHori;
            }

            @Override
            public float getHeight(Size size, Length length) {
                return size.value/2;
            }

            @Override
            public Vector2 getLinearVelocity(float speed) {
                return new Vector2(-speed, 0f);
            }

            @Override
            public boolean getDeath(Body body, Size size, Length length) {
                return (body.getPosition().x <= -getHeight(size, length)) ? true : false;
            }
        };

        public abstract Vector2 getPosition(Random rand, Size size, Length length);
        public abstract Vector2 getLinearVelocity(float speed);
        public abstract float getWidth(Size size, Length length);
        public abstract float getHeight(Size size, Length length);
        public abstract boolean getDeath(Body body, Size size, Length length);

        public static Direction getRandomDirection() {
            Random rand = new Random();
             return values()[rand.nextInt(values().length)];
        }
    }

    public enum Length {
        THIN(1f, 1f),
        SHORT(Constants.WORLD_WIDTH/8, Constants.WORLD_HEIGHT/8),
        MEDIUM(Constants.WORLD_WIDTH/4, Constants.WORLD_HEIGHT/4),
        LONG(Constants.WORLD_WIDTH/2, Constants.WORLD_HEIGHT/2);

        public final float valueHori, valueVert;

        private Length(float valueHori, float valueVert) {
            this.valueHori = valueHori;
            this.valueVert = valueVert;
        }
    }

    public enum Size {
        TINY(1f),
        SMALL(5f),
        MEDIUM(10f),
        BIG(15f);

        public final float value;

        private Size(float value) {
            this.value = value;
        }
    }

    public Direction direction;
    public Size size;
    public Length length;
    public float speed;
    public Body body;

    public Wall(GameWorld gameWorld, Size size, Length length) {
        this.direction = Direction.getRandomDirection();
        this.size = size;
        this.length = length;
        this.speed = 20f;
        initWall(gameWorld, direction, size, length);
    }

    public Wall(GameWorld gameWorld, Direction direction, Size size, Length length) {
        this.direction = direction;
        this.size = size;
        this.length = length;
        this.speed = 20f;
        initWall(gameWorld, direction, size, length);
    }

    private void initWall(GameWorld gameWorld, Direction direction, Size size, Length length) {
        BodyDef bodyDef = new BodyDef();
        bodyDef.type = BodyType.KinematicBody;
        bodyDef.position.set(new Vector2(2, 15));  

        PolygonShape box = new PolygonShape();
        Random rand = new Random();

        box.setAsBox(direction.getWidth(size, length), direction.getHeight(size, length));
        bodyDef.position.set(direction.getPosition(rand, size, length));
        body = gameWorld.world.createBody(bodyDef);
        body.setLinearVelocity(direction.getLinearVelocity(speed));

        body.createFixture(box, 0.0f); 
        box.dispose();
    }
}
\$\endgroup\$
3
  • \$\begingroup\$ Can you give an example of the usage that's giving you problems? In other words, what do the clients of this code look like? \$\endgroup\$
    – Eric Stein
    Commented Dec 28, 2015 at 19:27
  • \$\begingroup\$ @Eric Stein theres no usage Problem i was just thinking thaz my way using the direction enums to handle everything the wall does by passjng size oder lengtj into the enums all the time, that this is cluncy and if this is Bad design? It works, vut i think the design is Bad \$\endgroup\$ Commented Dec 28, 2015 at 21:49
  • \$\begingroup\$ Please do not add, remove, or edit code in a question after you've received an answer. The site policy is explained in What to do when someone answers. \$\endgroup\$
    – Mast
    Commented Dec 29, 2015 at 17:30

2 Answers 2

4
\$\begingroup\$

1. Missing model

You are missing a model or if it is there it is mixed together with the UI. For example: You have an Enum "Size" that internally holds UI information (float) and externally promotes a model fragment (TINY, SMALL, ...).

I would force myself to write the "game" without any UI first so I have the essence decoupled.

So the first step for me would be to separate those fragments.

2. Idea to distribute responsibilities of Direction

Your Direction Enum owns responsibilities that it shouldn't. My suggestion is to represent the directions for 2D navigation as tuples of the factors -1, 0 and 1.

public enum Direction {

    UP(0, -1), DOWN(0, 1), LEFT(-1, 0), RIGHT(1, 0);

    private Direction(float x, float y) {
        this.x = x;
        this.y = y;
    }

    private float x;
    private float y;

    public float getX() {
        return x;
    }
    public float getY() {
        return y;
    }

}

For example to calculate the linear velocity now only ONE method is necessary:

public Vector2 getLinearVelocity(float speed, Direction direction) {
    return new Vector2(speed * direction.getX(), speed * direction.getY());
}

The trick is to use the factor 0 to wipe out the summands not needed and to keep the important summands with -1 or 1. For the other methods I think it will be much more effort. But it's worth a try.

\$\endgroup\$
9
  • \$\begingroup\$ ok, I will try your second idea and see if it works, for the missing model, you think I should wait with things like tiny,long,short, big etc. remove them for now and just use direct values until i got the basic game? \$\endgroup\$ Commented Dec 29, 2015 at 10:49
  • \$\begingroup\$ I did implements the 2D navigation as tuples for the directions, getDeath() now looks like this imgur.com/5kNYkIX, but its ok I guess, as long as I dont need four classes for every direction or something \$\endgroup\$ Commented Dec 29, 2015 at 15:12
  • \$\begingroup\$ Using tiny, long, short etc in your model is totally ok. But any information related to a UI like pixels or so should not occur within your model. Tiny, long etc. should be interpreted by the UI. Pixels may vary on different UIs. The way to start with the model prevents me from putting logic into the UI, the UI is not responsible for. You also can develop the UI simultanously but the other way it enforces clean layers. \$\endgroup\$
    – oopexpert
    Commented Dec 29, 2015 at 16:20
  • \$\begingroup\$ The getDeath()-method is something what I expected. But that doesn't matter. This method shows me that there is a hidden structure we are not aware of yet. Assuming that "Direction" has now the right semantic something prevents us from writing clean code in getDeath(). A rough assumption is that it has something to do with 90 degrees angulars or horizontal or vertical displacement of the "Wall". Because getDeath() uses the dimension of the wall to determine death the problem begins with getWidth() and getHeight(). \$\endgroup\$
    – oopexpert
    Commented Dec 29, 2015 at 16:30
  • \$\begingroup\$ Your "Length" distinguishes between horizontal and vertical. "Size" does not. But after all a walls' dimension consists of width and height (a box) with the right angles. As the angles are not taken into consideration somewhere the code has to formulate it to meet the requirement of the gameplay. Let me and others think about that for a while. I am have no problem with reverting question acceptence. \$\endgroup\$
    – oopexpert
    Commented Dec 29, 2015 at 16:39
2
\$\begingroup\$

Since size, length, and body are public, you can just pass the Wall to the Direction instead. Also, Direction has so much functionality that it might as well be a class rather than an enum. In fact, it would be even better if it's an interface:

import java.util.Random;

import com.AndroidTest.game.Constants;
import com.AndroidTest.game.GameWorld.GameWorld;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.physics.box2d.Body;
import com.badlogic.gdx.physics.box2d.BodyDef;
import com.badlogic.gdx.physics.box2d.BodyDef.BodyType;
import com.badlogic.gdx.physics.box2d.PolygonShape;

/*
 * Replaces the Wall.Direction enum.
 *
 * The Wall is passed as a parameter to avoid a circular
 * dependency between Wall and Direction.
 */
public interface Direction {
    Vector2 getPosition(Wall wall, Random rand);
    float getWidth(Wall wall);
    float getHeight(Wall wall);
    Vector2 getLinearVelocity(float speed);
    boolean getDeath(Wall wall);
}
/*
 * A Direction implementation.
 */
public class UpDirection {
    public Vector2 getPosition(Wall wall, Random rand) {
        return new Vector2(rand.nextFloat()*(Constants.WORLD_WIDTH-wall.size.value)+wall.size.value/2, -wall.length.valueVert);
    }

    float getWidth(Wall wall) {
        return wall.size.value/2;
    }

    float getHeight(Wall wall) {
        return wall.length.valueVert;
    }

    Vector2 getLinearVelocity(float speed) {
        return new Vector2(0f, speed);
    }

    boolean getDeath(Wall wall) {
        return (wall.body.getPosition().y >= Constants.WORLD_HEIGHT+getHeight(wall)) ? true : false;
    }
}

/*
 * The remaining three Direction implementations go here.
 */

/*
 * Direction should not know about it's implementations,
 * so this factory is used instead to randomly create a Direction instance.
 */
public class DirectionFactory {
    public static Direction getRandomDirection() {
        Random rand = new Random();

        switch(rand.nextInt(4)) {
            case 0: 
                return new UpDirection()
                break;
            case 0: 
                return new DownDirection()
                break;
            case 0: 
                return new RightDirection()
                break;
            default: 
                return new LeftDirection()
                break;
        }
    }
}

public class Wall {   

    public enum Length {
        THIN(1f, 1f),
        SHORT(Constants.WORLD_WIDTH/8, Constants.WORLD_HEIGHT/8),
        MEDIUM(Constants.WORLD_WIDTH/4, Constants.WORLD_HEIGHT/4),
        LONG(Constants.WORLD_WIDTH/2, Constants.WORLD_HEIGHT/2);

        public final float valueHori, valueVert;

        private Length(float valueHori, float valueVert) {
            this.valueHori = valueHori;
            this.valueVert = valueVert;
        }
    }

    public enum Size {
        TINY(1f),
        SMALL(5f),
        MEDIUM(10f),
        BIG(15f);

        public final float value;

        private Size(float value) {
            this.value = value;
        }
    }

    public Direction direction;
    public Size size;
    public Length length;
    public float speed;
    public Body body;

    public Wall(GameWorld gameWorld, Size size, Length length) {
        this(gameWorld, DirectionFactory.getRandomDirection(), size, length) // Delegate to the other constructor
    }

    public Wall(GameWorld gameWorld, Direction direction, Size size, Length length) {
        this.direction = direction;
        this.size = size;
        this.length = length;
        this.speed = 20f;
        initWall(gameWorld); // initWall() has access to direction, size, and length.
    }

    private void initWall(GameWorld gameWorld) {
        BodyDef bodyDef = new BodyDef();
        bodyDef.type = BodyType.KinematicBody;
        bodyDef.position.set(new Vector2(2, 15));  

        PolygonShape box = new PolygonShape();
        Random rand = new Random();

        box.setAsBox(direction.getWidth(this), direction.getHeight(this)); // Pass Wall (this) to Direction.
        bodyDef.position.set(direction.getPosition(wall, rand)); // Here too.
        body = gameWorld.world.createBody(bodyDef);
        body.setLinearVelocity(direction.getLinearVelocity(speed));

        body.createFixture(box, 0.0f); 
        box.dispose();
    }
}

In this example, Direction is an interface with four implementations, (only one of which I coded), one for each of the possible directions. The methods accept a Wall and any other necessary parameters which are not part of the Wall. The implementations can then get size, length, and body from the Wall. Sure, this breaks some object-oriented programming rules, but as the adage says: some rules are meant to be broken :) Note that it still involves passing the current state of the Wall.

Alternatively, you can implement Wall as an abstract class containing void initWall(GameWorld gameWorld) and the equivalent of the Direction methods as abstract methods. Then, you can remove Direction all together and implement four Walls; one for each direction.

\$\endgroup\$
1
  • \$\begingroup\$ InitWall uses gameWorld which is not a property of Wall, so it must be passed in. \$\endgroup\$ Commented Dec 29, 2015 at 12:18

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.