The game is written in Java and built on libgdx, so if you're completely unfamiliar with that and how it structures things you may want to look into that first. I also use the Artemis Entity-Component-Systems framework, which I'll explain a little bit but which you can read more about at their site. (As of writing, Artemis's site is down for some reason, hopefully they'll have it back up soon)
All of the main game code is in the ld2710seconds project. There are also ld2710seconds-desktop and ld2710seconds-android projects - this is how libgdx sets things up. The android and desktop projects each contain very little code - just enough to set up an instance of the game. The android project also contains all of the game assets under its "assets" folder - this is the folder referenced by the other project as well, and is necessary because of some weird android reason.
The main (and only) screen of the game is found in the GameScreen class under the ld2710seconds.screens package. This is the class that, at a high level, is responsible for setting up the game and rendering it every frame. Let's take a look at GameScreen.java
1: package ld2710seconds.screens;
2: import ld2710seconds.systems.BoxRenderSystem;
3: import ld2710seconds.systems.ControlSystem;
4: import ld2710seconds.systems.DeathSystem;
5: import ld2710seconds.systems.NinePatchRenderSystem;
6: import ld2710seconds.systems.RandomColorSystem;
7: import ld2710seconds.systems.SpriteRenderSystem;
8: import ld2710seconds.systems.TemporarySystem;
9: import ld2710seconds.systems.TextRenderSystem;
10: import ld2710seconds.systems.TimerSystem;
11: import ld2710seconds.systems.VelocitySystem;
12: import ld2710seconds.systems.WinSystem;
13: import ld2710seconds.systems.XboxControlSystem;
14: import ld2710seconds.utils.EntityFactory;
15: import ld2710seconds.utils.LevelFactory;
16: import com.artemis.World;
17: import com.artemis.managers.GroupManager;
18: import com.badlogic.gdx.Gdx;
19: import com.badlogic.gdx.graphics.OrthographicCamera;
20: import com.badlogic.gdx.math.Vector2;
21: import com.badlogic.gdx.physics.box2d.Box2DDebugRenderer;
22: public class GameScreen extends BaseScreen {
23: private OrthographicCamera camera;
24: private OrthographicCamera pixelPerfectCamera;
25: private OrthographicCamera halfCamera;
26: public static int WIDTH = 32;
27: public static int HEIGHT = (int) ((WIDTH / (float) Gdx.graphics.getWidth()) * Gdx.graphics
28: .getHeight());
29: World world;
30: com.badlogic.gdx.physics.box2d.World physicsWorld;
31: Box2DDebugRenderer debugRenderer;
32: public GameScreen() {
33: setupCamera();
34: setupWorld();
35: debugRenderer = new Box2DDebugRenderer();
36: }
37: @Override
38: public void show() {
39: // TODO Auto-generated method stub
40: super.show();
41: //Gdx.graphics.setTitle("Captain Timewarp");
42: }
43: private void setupCamera() {
44: camera = new OrthographicCamera();
45: camera.setToOrtho(false, WIDTH, HEIGHT);
46: pixelPerfectCamera = new OrthographicCamera();
47: pixelPerfectCamera.setToOrtho(false, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
48: halfCamera = new OrthographicCamera();
49: halfCamera.setToOrtho(false, Gdx.graphics.getWidth()/4, Gdx.graphics.getHeight()/4);
50: }
51: private void setupWorld() {
52: world = new World();
53: physicsWorld = new com.badlogic.gdx.physics.box2d.World(new Vector2(0,
54: -19.62f), true);
55: EntityFactory.init(world, physicsWorld);
56: world.setManager(new GroupManager());
57: world.setSystem(new VelocitySystem());
58: world.setSystem(new BoxRenderSystem(camera));
59: world.setSystem(new ControlSystem());
60: world.setSystem(new SpriteRenderSystem(camera));
61: world.setSystem(new NinePatchRenderSystem(camera, halfCamera));
62: world.setSystem(new TextRenderSystem(camera, pixelPerfectCamera));
63: world.setSystem(new TemporarySystem());
64: world.setSystem(new RandomColorSystem());
65: world.setSystem(new XboxControlSystem(null));
66: //world.setSystem(new OuyaControlSystem(null));
67: // world.setSystem(new ControllerDebugSystem());
68: world.setSystem(new TimerSystem());
69: world.setSystem(new WinSystem());
70: world.setSystem(new DeathSystem());
71: world.initialize();
72: //EntityFactory.createControllerDebugInfo();
73: LevelFactory.createLevelOne();
74: }
75: public void render(float delta) {
76: super.render(delta);
77: physicsWorld.step(delta, 16, 16);
78: world.setDelta(delta);
79: world.process();
80: //debugRenderer.render(physicsWorld, camera.combined);
81: }
82: }
There are a few key things to note here. At lines 26 and 27 we initialize the WIDTH and HEIGHT constants. WIDTH is set to be 32, and for HEIGHT we use an equation to determine the proper height that would maintain the aspect ratio relative to a width of 32. For our screen size of 1024x640, this means that our HEIGHT will be 20. This is used to logically divide up the screen into 32x20, so I can operate in a pixel-independent world space rather than caring about pixels.
The GameScreen constructor calls out to two methods, setupCamera() and setupWorld(). It also initializes a Box2DDebugRenderer that I commented out anyway, so it doesn't matter.
setupCamera() initializes our main camera "camera" to the WIDTH and HEIGHT that we computed earlier. I also initialize a "pixelPerfectCamera" to the actual pixel dimensions of the screen, and a "halfCamera" to the quartered dimensions of the screen - these are used in instances where libgdx needs to operate on actual pixel sizes, or where my very small 32x20 world causes issues.
setupWorld() initializes our Artemis World (called "world") and our Box2D physics world (called "physicsWorld"). The constructor for physicsWorld takes in a vector that sets up our gravity. We then pass these along statically to our EntityFactory class, which will be very important later. Then we initialize all of the Systems that our World uses. These systems are used to do almost all of our major game logic. For example, the SpriteRenderSystem is responsible for drawing any sprite to the screen (see how it takes in our camera?). The TemporarySystem is responsible for removing things that are supposed to be "temporary", such as particle effects. setupWorld also starts the game by creating level one.
The render() method is overridden from BaseScreen (which implements it from the Screen interface), and is called every frame. The render method takes the place of a traditional game loop - this is where all of the updating and drawing happens. In GameScreen, we first make the call to super (this handles clearing the screen), then step our physicsWorld according to the delta (the amount of time that has elapsed since the last frame), then set that delta on our world, and then tell the world to process. The call to process() makes all of our Systems run, and thus is responsible for starting off all of the drawing, updating, and other game logic.
Let's now take a look at the EntityFactory class. EntityFactory is a factory class with a bunch of static methods for producing entities in our gameworld.
1: package ld2710seconds.utils;
2: import java.util.Random;
3: import ld2710seconds.components.BodyComponent;
4: import ld2710seconds.components.BoxComponent;
5: import ld2710seconds.components.ControllerDebugComponent;
6: import ld2710seconds.components.ControlsComponent;
7: import ld2710seconds.components.GoalComponent;
8: import ld2710seconds.components.NinePatchComponent;
9: import ld2710seconds.components.PositionComponent;
10: import ld2710seconds.components.RandomColorComponent;
11: import ld2710seconds.components.SpawnPointComponent;
12: import ld2710seconds.components.SpriteComponent;
13: import ld2710seconds.components.TemporaryComponent;
14: import ld2710seconds.components.TextComponent;
15: import ld2710seconds.components.TimerComponent;
16: import ld2710seconds.components.VelocityComponent;
17: import ld2710seconds.screens.GameScreen;
18: import com.artemis.Entity;
19: import com.artemis.World;
20: import com.artemis.managers.GroupManager;
21: import com.artemis.utils.ImmutableBag;
22: import com.badlogic.gdx.Gdx;
23: import com.badlogic.gdx.Input.Keys;
24: import com.badlogic.gdx.graphics.Color;
25: import com.badlogic.gdx.graphics.Texture;
26: import com.badlogic.gdx.graphics.g2d.NinePatch;
27: import com.badlogic.gdx.graphics.g2d.Sprite;
28: import com.badlogic.gdx.math.Vector2;
29: import com.badlogic.gdx.physics.box2d.Body;
30: import com.badlogic.gdx.physics.box2d.BodyDef;
31: import com.badlogic.gdx.physics.box2d.Fixture;
32: import com.badlogic.gdx.physics.box2d.PolygonShape;
33: import com.badlogic.gdx.utils.Array;
34: public class EntityFactory {
35: private static final int PARTICLE_SPEED = 8;
36: static World world;
37: static com.badlogic.gdx.physics.box2d.World physicsWorld;
38: static Random rand;
39: public static void init(World world,
40: com.badlogic.gdx.physics.box2d.World physicsWorld) {
41: EntityFactory.world = world;
42: EntityFactory.physicsWorld = physicsWorld;
43: rand = new Random();
44: }
45: public static void createFloor(float x, float y, float w, float h,
46: Color color) {
47: BodyDef bodyDef = new BodyDef();
48: bodyDef.type = BodyDef.BodyType.StaticBody;
49: bodyDef.position.set(x, y);
50: Body body = physicsWorld.createBody(bodyDef);
51: PolygonShape shape = new PolygonShape();
52: shape.setAsBox(w / (2), h / (2));
53: body.setUserData("floor");
54: body.createFixture(shape, 20);
55: BodyComponent bodyComponent = new BodyComponent(body);
56: PositionComponent positionComponent = new PositionComponent(
57: new Vector2(x, y));
58: positionComponent.setBody(bodyComponent);
59: BoxComponent boxComponent = new BoxComponent(w, h, color);
60: NinePatchComponent nineComponent = new NinePatchComponent(w, h,
61: new NinePatch(new Texture(Gdx.files.internal("data/tile.png")),
62: 6, 6, 6, 6));
63: Entity entity = world.createEntity();
64: entity.addComponent(bodyComponent);
65: entity.addComponent(positionComponent);
66: entity.addComponent(nineComponent);
67: entity.addComponent(boxComponent);
68: world.getManager(GroupManager.class).add(entity, "all");
69: world.addEntity(entity);
70: }
71: public static void makeFrame() {
72: PositionComponent positionComponent = new PositionComponent(
73: new Vector2(GameScreen.WIDTH / 2, GameScreen.HEIGHT / 2));
74: // BoxComponent boxComponent = new BoxComponent(GameScreen.WIDTH,
75: // GameScreen.HEIGHT, color);
76: NinePatchComponent nineComponent = new NinePatchComponent(
77: GameScreen.WIDTH, GameScreen.HEIGHT, new NinePatch(new Texture(
78: Gdx.files.internal("data/frame.png")), 8, 8, 8, 8));
79: Entity entity = world.createEntity();
80: entity.addComponent(positionComponent);
81: entity.addComponent(nineComponent);
82: world.getManager(GroupManager.class).add(entity, "all");
83: world.addEntity(entity);
84: }
85: public static void createFrozenPlayer(float x, float y, boolean flip) {
86: BodyDef bodyDef = new BodyDef();
87: bodyDef.type = BodyDef.BodyType.StaticBody;
88: bodyDef.position.set(x, y);
89: Body body = physicsWorld.createBody(bodyDef);
90: PolygonShape shape = new PolygonShape();
91: shape.setAsBox(.4f, .5f);
92: body.setUserData("floor");
93: Fixture fix =body.createFixture(shape, 20);
94: fix.setFriction(0f);
95: BodyComponent bodyComponent = new BodyComponent(body);
96: PositionComponent positionComponent = new PositionComponent(
97: new Vector2(x, y));
98: positionComponent.setBody(bodyComponent);
99: // BoxComponent boxComponent = new BoxComponent(1f, 1f, color);
100: SpriteComponent spriteComponent = new SpriteComponent(1f, 1f,
101: new Sprite(new Texture(
102: Gdx.files.internal("data/character_g.png"))));
103: spriteComponent.setFlipX(flip);
104: Entity entity = world.createEntity();
105: entity.addComponent(bodyComponent);
106: entity.addComponent(positionComponent);
107: entity.addComponent(spriteComponent);
108: entity.addComponent(new TemporaryComponent(10f));
109: world.getManager(GroupManager.class).add(entity, "all");
110: world.addEntity(entity);
111: entity = world.createEntity();
112: entity.addComponent(new PositionComponent(new Vector2(x, y - .5f)));
113: entity.addComponent(new TextComponent("10", .5f));
114: entity.addComponent(new TemporaryComponent(10f));
115: entity.addComponent(new TimerComponent());
116: world.getManager(GroupManager.class).add(entity, "all");
117: entity.addToWorld();
118: }
119: public static void createParticles(int num, float x, float y, Color color) {
120: for (int i = 0; i < num; i++) {
121: float angle = (360f / num) * i;
122: Vector2 dir = new Vector2(1, 1);
123: dir = dir.setAngle(angle);
124: dir = dir.nor();
125: dir = dir.scl(PARTICLE_SPEED).cpy();
126: // float xDir = (float) (rand.nextFloat() * (2*PARTICLE_SPEED) -
127: // PARTICLE_SPEED);
128: // float yDir = (float) (rand.nextFloat() * (2*PARTICLE_SPEED) -
129: // PARTICLE_SPEED);
130: PositionComponent positionComponent = new PositionComponent(
131: new Vector2(x, y));
132: BoxComponent boxComponent = new BoxComponent(.1f, .1f, color);
133: VelocityComponent velocityComponent = new VelocityComponent(dir);
134: Entity entity = world.createEntity();
135: entity.addComponent(positionComponent);
136: entity.addComponent(boxComponent);
137: entity.addComponent(velocityComponent);
138: entity.addComponent(new RandomColorComponent());
139: entity.addComponent(new TemporaryComponent(.75f));
140: world.getManager(GroupManager.class).add(entity, "all");
141: world.addEntity(entity);
142: }
143: }
144: public static void createControllerDebugInfo()
145: {
146: Entity entity = world.createEntity();
147: entity.addComponent(new PositionComponent(new Vector2(2,5)));
148: entity.addComponent(new TextComponent("TEST", .25f));
149: entity.addComponent(new ControllerDebugComponent());
150: entity.addToWorld();
151: }
152: public static void createPlayer(float x, float y) {
153: BodyDef bodyDef = new BodyDef();
154: bodyDef.type = BodyDef.BodyType.DynamicBody;
155: bodyDef.position.set(x, y);
156: Body body = physicsWorld.createBody(bodyDef);
157: PolygonShape shape = new PolygonShape();
158: shape.setAsBox(.7f / (2), 1f / (2));
159: Fixture fix = body.createFixture(shape, 40);
160: fix.setFriction(.11f);
161: //body.setLinearDamping(5f);
162: body.setFixedRotation(true);
163: BodyComponent bodyComponent = new BodyComponent(body);
164: PositionComponent positionComponent = new PositionComponent(
165: new Vector2(x, y));
166: positionComponent.setBody(bodyComponent);
167: // BoxComponent boxComponent = new BoxComponent(.8f, 1f, Color.GREEN);
168: ControlsComponent controlsComponent = new ControlsComponent(Keys.W,
169: Keys.A, Keys.D, Keys.S);
170: SpriteComponent spriteComponent = new SpriteComponent(1f, 1f,
171: new Sprite(
172: new Texture(Gdx.files.internal("data/character.png"))));
173: Entity entity = world.createEntity();
174: entity.addComponent(bodyComponent);
175: entity.addComponent(positionComponent);
176: // entity.addComponent(boxComponent);
177: entity.addComponent(controlsComponent);
178: entity.addComponent(new SpawnPointComponent(x, y));
179: entity.addComponent(spriteComponent);
180: world.getManager(GroupManager.class).add(entity, "player");
181: world.getManager(GroupManager.class).add(entity, "all");
182: world.addEntity(entity);
183: createParticles(20, x, y, Color.BLUE);
184: }
185: public static void freezeAndReset(SpawnPointComponent spawn,
186: BodyComponent body, Entity entity, boolean flip) {
187: createFrozenPlayer(body.getX(), body.getY(), flip);
188: createParticles(20, body.getX(), body.getY(), Color.BLUE);
189: physicsWorld.destroyBody(body.getBody());
190: resetPlayer(spawn, entity);
191: }
192: public static void resetPlayer(SpawnPointComponent spawn, Entity entity)
193: {
194: createPlayer(spawn.getX(), spawn.getY());
195: world.deleteEntity(entity);
196: }
197: public static void createGoalPoint(float x, float y) {
198: BoxComponent box = new BoxComponent(1f, 2f, Color.WHITE);
199: RandomColorComponent randomColor = new RandomColorComponent();
200: PositionComponent position = new PositionComponent(new Vector2(x, y));
201: SpriteComponent sprite = new SpriteComponent(1, 2f, new Sprite(
202: new Texture(Gdx.files.internal("data/door.png"))));
203: Entity entity = world.createEntity();
204: entity.addComponent(box);
205: entity.addComponent(position);
206: entity.addComponent(randomColor);
207: entity.addComponent(sprite);
208: entity.addComponent(new GoalComponent());
209: world.getManager(GroupManager.class).add(entity, "goal");
210: world.getManager(GroupManager.class).add(entity, "all");
211: entity.addToWorld();
212: }
213: public static void resetWorld() {
214: GroupManager group = world.getManager(GroupManager.class);
215: ImmutableBag<Entity> entities = group.getEntities("all");
216: for (int i = 0; i < entities.size(); i++)
217: {
218: world.deleteEntity(entities.get(i));
219: }
220: Array<Body> bodies = new Array<Body>();
221: physicsWorld.getBodies(bodies);
222: for(Body body : bodies)
223: {
224: physicsWorld.destroyBody(body);
225: }
226: }
227: }
As you can see, EntityFactory has a bunch of "createX" methods that are responsible for taking in some data and producing game entities. All of these methods follow the same basic structure:
- Create an Entity instance from the world (using world.createEntity())
- Add some Components to it. Components hold all the pieces of data necessary to represent an object.
- Add the entity to the world.
If you're used to traditional object-oriented structures, you'll have noticed by now that the game doesn't have any notion of a Player class, or anything kind of major class hierarchy. The Entity Component System architecture relies on composition rather than inheritance, so everything in the game world is just an Entity with various components attached.
For example, let's take a look at the createPlayer method (line 152). This method takes in two floats to represent the x/y coordinates that the player will be created at. First we create the physics body of the player (lines 153-162). For more information on this look into Box2D. We then make a BodyComponent with that Body - this will be added to our entity later. Then we make a PositionComponent, giving it a Vector2 containing the x and y coordinates. We then tell our PositionComponent to be managed by the BodyComponent using the setBody method. Take a look at these components under the ld2710seconds.components package to see how this works - basically it just means that the BodyComponent's position will override the PositionComponent's position. Then we make the ControlsComponent, passing it the keys that we want to control our player. While we're not doing it here, we could use variables instead of the hardcoded key values to allow for rebindable keys. It would also allow for multiple local players if we gave another Entity another set of controls in its ControlsComponent. Then we add a SpriteComponent, which takes in two floats for the x and y size of the sprite, and a Sprite instance. Note that while our sprite is 16x16 pixels, we're rendering it as 1x1 - this is how our camera will see it. We add these components to the Entity, then add the entity to the world. This means that the Systems in our world will be able to operate on this entity.
Let's now take a look at one of our Systems. This is the SpriteRenderSystem, which is responsible for drawing all of the Entities in our World that have SpriteComponents
1: package ld2710seconds.systems;
2: import ld2710seconds.components.PositionComponent;
3: import ld2710seconds.components.SpriteComponent;
4: import com.artemis.Aspect;
5: import com.artemis.ComponentMapper;
6: import com.artemis.Entity;
7: import com.artemis.annotations.Mapper;
8: import com.artemis.systems.EntityProcessingSystem;
9: import com.badlogic.gdx.graphics.OrthographicCamera;
10: import com.badlogic.gdx.graphics.g2d.SpriteBatch;
11: public class SpriteRenderSystem extends EntityProcessingSystem {
12: @Mapper ComponentMapper<PositionComponent> positionMapper;
13: @Mapper ComponentMapper<SpriteComponent> spriteMapper;
14: private OrthographicCamera camera;
15: private SpriteBatch batch;
16: public SpriteRenderSystem(OrthographicCamera camera) {
17: super(Aspect.getAspectForAll(PositionComponent.class, SpriteComponent.class));
18: // TODO Auto-generated constructor stub
19: this.camera = camera;
20: this.batch = new SpriteBatch();
21: }
22: @Override
23: protected void begin() {
24: // TODO Auto-generated method stub
25: super.begin();
26: camera.update();
27: batch.setProjectionMatrix(camera.combined);
28: batch.begin();
29: }
30: @Override
31: protected void process(Entity entity) {
32: SpriteComponent sprite = spriteMapper.get(entity);
33: PositionComponent position = positionMapper.get(entity);
34: batch.draw(sprite.getSprite(), position.getPosition().x-sprite.getW()/2, position.getPosition().y-sprite.getH()/2, sprite.getW(), sprite.getH());
35: }
36: @Override
37: protected void end() {
38: // TODO Auto-generated method stub
39: super.end();
40: batch.end();
41: }
42: }
This class extends EntityProcessingSystem, which means that it gets called every frame to operate on a set of Entities. We determine which Entities we want via an Aspect, as shown in line 17. This line tells the System that we want to operate on every Entity that has a PositionComponent and a SpriteComponent. Note that we don't know specifically what each thing might be - they're all Entities. So everything in the world with those two components will be treated the same.
The begin() method is called once every frame before the System does any work. Here we use it to tell our SpriteBatch to begin, and update its projection matrix in case the camera changed. In this particular game our camera never moves, but if we had a moving camera this would be necessary to make it work. Similarly, the end() method is called after the system is done with all of its work, and we use it to end the SpriteBatch. The "meat" of the System is the process() method, which gets called once with each Entity that we're processing. Here what we do is grab the SpriteComponent and the PositionComponent, then use our SpriteBatch to draw that sprite at that position, doing a little math to ensure that the Sprite is centered.
This is just a small vertical slice of the code in this game, but it should kind of give you a feel for how everything is structured. Feel free to look through the code and adapt it for your own use. Also, if you want to know anything else about how anything works, let me know in the comments.
The begin() method is called once every frame before the System does any work. Here we use it to tell our SpriteBatch to begin, and update its projection matrix in case the camera changed. In this particular game our camera never moves, but if we had a moving camera this would be necessary to make it work. Similarly, the end() method is called after the system is done with all of its work, and we use it to end the SpriteBatch. The "meat" of the System is the process() method, which gets called once with each Entity that we're processing. Here what we do is grab the SpriteComponent and the PositionComponent, then use our SpriteBatch to draw that sprite at that position, doing a little math to ensure that the Sprite is centered.
This is just a small vertical slice of the code in this game, but it should kind of give you a feel for how everything is structured. Feel free to look through the code and adapt it for your own use. Also, if you want to know anything else about how anything works, let me know in the comments.
Also also, I apologize if anything I did here was wrong/stupid/shitty/amateurish.
No comments:
Post a Comment