I have written a small LibGDX program attempting to implement the observer pattern to help with object decoupling. It needs work however and I would like some help with it. Here is the source code. My questions are below.
public class ObserverTest extends ApplicationAdapter implements Observer
{
private SpriteBatch batch;
private List<GameObject> gameObjects;
public static int frameCount = 0;
@Override
public void create()
{
batch = new SpriteBatch();
gameObjects = new ArrayList<GameObject>();
gameObjects.add(new Ship(new Vector2(Gdx.graphics.getWidth() / 2, Gdx.graphics.getHeight() / 2),
new Vector2(50, 50), 5.0, 0.0, 5.0, Color.RED));
// After all objects have been added
for(GameObject gameObject : gameObjects)
{
if(gameObject instanceof Subject)
{
((Subject)gameObject).addObserver(this);
}
}
}
@Override
public void render()
{
Gdx.gl.glClearColor(0, 0, 0, 1);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
// Why does this not work when adding new objects while iterating?
/*for(GameObject gameObject : gameObjects)
{
gameObject.update();
}*/
// Why does this work when adding new objects while iterating?
for(int i = 0; i < gameObjects.size(); ++i)
{
gameObjects.get(i).update();
}
batch.begin();
for(GameObject gameObject : gameObjects)
{
gameObject.render(batch);
}
batch.end();
frameCount++;
// Will this work as I expect it to?
if(frameCount == Integer.MAX_VALUE)
{
frameCount = 0;
}
}
@Override
public void dispose()
{
batch.dispose();
}
@Override
public void respond(GameObject gameObject, Event event)
{
switch(event)
{
case SHIP_FIRED_BULLET:
{
gameObjects.add(gameObject);
break;
}
}
}
}
public abstract class GameObject
{
protected Vector2 position, dimension, direction;
protected double speed, angle, rotationSpeed;
protected Texture image;
protected GameObject()
{
position = new Vector2(0, 0);
dimension = new Vector2(0, 0);
direction = new Vector2(0, 0);
speed = 0.0;
angle = 0.0;
rotationSpeed = 0.0;
}
public abstract void update();
public abstract void render(SpriteBatch batch);
}
public class Ship extends GameObject implements Subject
{
private List<Observer> observers;
public Ship(Vector2 position, Vector2 dimension, double speed, double angle, double rotationSpeed, Color color)
{
this.position = position;
this.dimension = dimension;
this.speed = speed;
this.angle = angle;
this.rotationSpeed = rotationSpeed;
observers = new ArrayList<Observer>();
Pixmap pixmap = new Pixmap((int)dimension.x, (int)dimension.y, Pixmap.Format.RGBA8888);
pixmap.setColor(color);
pixmap.drawLine(0, 0, (int)dimension.x, (int)dimension.y / 2); // Outside left
pixmap.drawLine(0, (int)dimension.y, (int)dimension.x, (int)dimension.y / 2); // Outside right
pixmap.drawLine(0, 0, (int)dimension.x / 2, (int)dimension.y / 2); // Inside left
pixmap.drawLine(0, (int)dimension.y, (int)dimension.x / 2, (int)dimension.y / 2); // Inside right
image = new Texture(pixmap);
image.setFilter(Texture.TextureFilter.Linear, Texture.TextureFilter.Linear); // Reduces jagged lines
pixmap.dispose();
}
@Override
public void update()
{
direction.x = (float)Math.cos(Math.toRadians(angle));
direction.y = (float)Math.sin(Math.toRadians(angle));
direction.nor();
direction.scl((float)speed);
if(Gdx.input.isKeyPressed(Input.Keys.UP))
{
position.add(direction);
}
if(Gdx.input.isKeyPressed(Input.Keys.LEFT))
{
angle += rotationSpeed;
}
if(Gdx.input.isKeyPressed(Input.Keys.RIGHT))
{
angle -= rotationSpeed;
}
if(Gdx.input.isKeyPressed(Input.Keys.M) && ObserverTest.frameCount % 10 == 0)
{
// How to avoid creating a new Bullet each time?
Bullet bullet = new Bullet(new Vector2(position.x, position.y), new Vector2(10, 1),
dimension.x / 2, speed * 2, angle, Color.RED);
for(Observer observer : observers)
{
observer.respond(bullet, Event.SHIP_FIRED_BULLET);
}
}
}
@Override
public void render(SpriteBatch batch)
{
batch.draw(image,
position.x - dimension.x / 2, position.y - dimension.y / 2,
dimension.x / 2, dimension.y / 2, // Origin is center point for rotation
dimension.x, dimension.y,
1f, 1f,
(float)angle,
0, 0, (int)dimension.x, (int)dimension.y,
false, false);
}
@Override
public void addObserver(Observer observer)
{
observers.add(observer);
}
@Override
public void removeObserver(int position)
{
observers.remove(position);
}
}
public class Bullet extends GameObject
{
public Bullet(Vector2 position, Vector2 dimension, double displacement, double speed, double angle, Color color)
{
this.position = position;
this.dimension = dimension;
this.speed = speed;
this.angle = angle;
direction.x = (float)Math.cos(Math.toRadians(angle));
direction.y = (float)Math.sin(Math.toRadians(angle));
direction.nor();
direction.scl((float)speed);
// This is just to ensure there isn't a divide by 0 error
if(displacement != 0)
{
/*
Here I'm trying to position the Bullet, half the width of its Ship away
from its Ship (displacement), at a point determined by the Ships direction.
As it is this works currently, but I don't know why. The 10.0f is arbitrary.
If the Ships dimensions are changed, the position of the Bullet upon being
fired can become incorrect.
How can I set the Bullets initial position correctly?
*/
position.mulAdd(direction, (float)displacement / 10.0f);
}
Pixmap pixmap = new Pixmap((int)dimension.x, (int)dimension.y, Pixmap.Format.RGBA8888);
pixmap.setColor(color);
pixmap.drawLine(0, 0, (int)dimension.x, 0);
image = new Texture(pixmap);
image.setFilter(Texture.TextureFilter.Linear, Texture.TextureFilter.Linear); // Reduces jagged lines
pixmap.dispose();
}
@Override
public void update()
{
position.add(direction);
}
@Override
public void render(SpriteBatch batch)
{
batch.draw(image,
position.x - dimension.x / 2, position.y - dimension.y / 2,
dimension.x / 2, dimension.y / 2, // Origin is center point for rotation
dimension.x, dimension.y,
1f, 1f,
(float)angle,
0, 0, (int)dimension.x, (int)dimension.y,
false, false);
}
}
public enum Event
{
SHIP_FIRED_BULLET
}
public interface Observer
{
void respond(GameObject gameObject, Event event);
}
public interface Subject
{
void addObserver(Observer observer);
void removeObserver(int position);
}
Regarding the code above, primarily I would like to know:
- Have I implemented the observer pattern correctly? Can it be improved and if so, how?
- Is my implementation scalable, as far as can be perceived currently?
- Is it okay to make Subject an interface? Every Subject is required to have a List of Observers, but enforcing this by making Subject a class which holds that List of Observers does not seem feasible to me. It would be difficult if not impossible to fit Subject into the GameObject class hierarchy in a correct manner, at least as far as I can see. As such, currently I must remember to give each Subject its own List of Observers independently. Is this reasonable or suitable?
- How can I avoid creating a new Bullet each time the Ship fires? The Bullet needs certain information from its firing Ship such as position and angle and currently this is most easily done by creating a new one upon firing and passing the information to the Bullet via its constructor. I do not think this is a good solution however. I would rather somehow re-use existing Bullets upon firing.
- Why does a standard for loop work when adding new objects to gameObjects in ObserverTest while iterating over it, while a for each loop does not? (Question in the code)
- Will frameCount be set back to 0 upon reaching MAX_VALUE? (Question in the code)
- How can I properly place the Bullet relative to the Ships position? (Question in the code)
Are there any other details I might have missed that should be examined? Help with this would be much appreciated. Thank you.