Introduction
Our Lunar Lander game is somewhat playable by this point but it still lacks some key features. After all, it would be nice if we could detect collisions and determine if the lander has safely landed on the pad. Let’s see how our flexible Entity-Component system permits us to expand our game with minimal fuss.
Collision Detection
First, a frank disclaimer: the following collision detection algorithm is entirely inefficient. It’s kept simple for our basic teaching purposes here but is probably undesirable in a game of any scale. But that’s OK: E-C will permit you to swap in a much more advanced collision detection system when you’re ready. 🙂
Desired feature: we want to know if our lander has collided with the ground. By now you have been doing enough E-C with me to anticipate what we need: a new component that describes the “collideable” attribute. The lander itself is collideable, as is the ground.
class PolygonCollidable < Component attr_accessor :bounding_polygon def marshal_dump [@id] end def marshal_load(array) @id = array end end
Let’s imagine that any entity with a “collideable” component gains a bounding polygon: a simple, invisible, rectangular box that fits around the texture. If the texture rotates, so does the bounding polygon. And when two bounding polygons intersect, that defines a collision. The libGDX library provides us with some very nice polygon-computation methods, so we don’t need to write those on our own.
def update_bounding_polygons(entity_mgr, entities) entities.each do |e| spatial_component = entity_mgr.get_component_of_type(e, SpatialState) renderable_component = entity_mgr.get_component_of_type(e, Renderable) collidable_component = entity_mgr.get_component_of_type(e, PolygonCollidable) collidable_component.bounding_polygon = make_polygon(spatial_component.x, renderable_component.width, spatial_component.y, renderable_component.height, renderable_component.rotation, renderable_component.scale) end end
Now that we have the polygons computed, one way to accomplish the collision detection is via a wholly naive, O(n^2) algorithm where we loop over all “collideable” entities and compare each one with every other “collideable” entity.
def process_one_game_tick(delta, entity_mgr) collidable_entities=[] polygon_entities = entity_mgr.get_all_entities_with_component_of_type(PolygonCollidable) update_bounding_polygons(entity_mgr, polygon_entities) collidable_entities += polygon_entities bounding_areas={} collidable_entities.each do |e| bounding_areas[e]=entity_mgr.get_component_of_type(e, PolygonCollidable).bounding_polygon end # Naive O(n^2) bounding_areas.each_key do |entity| bounding_areas.each_key do |other| next if entity==other if Intersector.overlapConvexPolygons(bounding_areas[entity], bounding_areas[other]) if entity_mgr.get_tag(entity)=='p1_lander' || entity_mgr.get_tag(other)=='p1_lander' #puts "Intersection!" return true end end end end return false end
[Aside: the O(n^2) notation — pronounced “big-oh of n squared” — describes the computational efficiency of an algorithm. In this case the function scales per the square of the arguments. That’s pretty bad.]
Again, libGDX gave us a hand by providing an intersection-detection method.
Landing Detection
Another desired feature is knowing when the lander has safely touched down on the pad. In conversational terms, that means at least half of the lander is on the pad, and it doesn’t touch down too fast. (Later, perhaps, we could improve that to include a maximum rotation: say, no more than 15 degrees from the vertical. But we’ll keep things simple for now.)
As before, you probably already know what we need: two new components, “landable” and “pad”. Landable is assigned to an entity can be landed; pad is assigned to an entity that can be landed upon. Here again you can anticipate E-C’s flexibility: we could have multiple players — each “landable” — as well as multiple places to land on — each with “pad”. Neither of these needs any data.
class Landable < Component end
class Pad < Component end
Our system logic then becomes a matter of looping over the appropriate entities and doing a little swift calculation to see if the lander is located within the margin of safety. If so, a proper landing has occurred:
def process_one_game_tick(delta, entity_mgr) landable_entities = entity_mgr.get_all_entities_with_component_of_type(Landable) pad_entities = entity_mgr.get_all_entities_with_component_of_type(Pad) landable_entities.each do |entity| location_component = entity_mgr.get_component_of_type(entity, SpatialState) renderable_component = entity_mgr.get_component_of_type(entity, Renderable) bl_x = location_component.x bl_y = location_component.y bc_x = bl_x + (renderable_component.width/2) br_x = bl_x + renderable_component.width br_y = bl_y pad_entities.each do |pad| pad_loc_component = entity_mgr.get_component_of_type(pad, SpatialState) pad_rend_component = entity_mgr.get_component_of_type(pad, Renderable) ul_x = pad_loc_component.x ul_y = pad_loc_component.y+pad_rend_component.height #puts "lander x: #{bc_x} y: #{bl_y} / Pad x: #{ul_x} y: #{ul_y}" ur_x = ul_x+pad_rend_component.width ur_y = ul_y if (bl_y>=ul_y-PIXEL_FUDGE && bl_y <= ul_y+PIXEL_FUDGE) && ( bc_x>=ul_x && bc_x <= ur_x) && ( location_component.dy <= MAX_SPEED) return true end end return false end end
Avoid the Asteroids!
Let’s conclude this post in a fun way. We have all the pieces in place — rendering, collision detection, and so forth — so let’s leverage the flexibility of E-C to throw a wholly unplanned feature into the mix. Let’s program an asteroid system that randomly generates asteroids off-screen and hurls them across the player’s game window. The player, naturally, must avoid colliding with these.
def generate_new_asteroids(delta, entity_mgr) if rand(50)==0 starting_x = -100 starting_y = rand(500) - 150 starting_dx = rand(15) + 2 starting_dy = rand(20) - 10 asteroid = entity_mgr.create_tagged_entity('asteroid') entity_mgr.add_component asteroid, SpatialState.new(starting_x, starting_y, starting_dx, starting_dy) entity_mgr.add_component asteroid, Renderable.new(RELATIVE_ROOT + "res/images/asteroid.png", 1.0, 0) entity_mgr.add_component asteroid, PolygonCollidable.new entity_mgr.add_component asteroid, Motion.new end end
Naturally we don’t want to bog down the machine with asteroids that have left the playing field, so after they have left the visible area we can clean them up:
def cleanup_asteroids(delta, entity_mgr) asteroid_entities = entity_mgr.get_all_entities_with_tag('asteroid') || [] asteroid_entities.each do |a| spatial_component = entity_mgr.get_component_of_type(a, SpatialState) if spatial_component.x > 640 entity_mgr.kill_entity(a) end end end
def process_one_game_tick(delta, entity_mgr) generate_new_asteroids(delta, entity_mgr) cleanup_asteroids(delta, entity_mgr) end
We didn’t have to refactor or re-think a single part of our existing codebase; we merely leveraged our existing spatial, renderable, collideable and motion components. We easily exploited E-C’s inherent flexibility to adapt our code to a wholly new, unanticipated feature.
Conclusion
Don’t forget, the full source code for this series is on Github.
I hope you enjoyed this series on Entity-Component programming as much as I enjoyed writing it. If you have questions, please get in touch. Thanks for reading.
Great blog series, thanks!
Very very good. I enjoyed to learn a new design pattern.
I made 2 games in java + slick2D (space invaders + snake) without any knowledge about game design : I failled in every place where E-C win….. so thank you a lot.