├── .ruby-version ├── 01-hello ├── hello_animation.rb ├── hello_movement.rb ├── hello_sound.rb └── hello_world.rb ├── 02-warmup ├── coordinate_system.rb ├── coordinate_system_with_rotation.rb ├── island.json ├── island.rb ├── perlin_noise_map.rb ├── player_movement.rb ├── random_map.rb ├── tiled_map.json └── tileset.rb ├── 03-prototype ├── README.md ├── entities │ ├── bullet.rb │ ├── camera.rb │ ├── explosion.rb │ ├── map.rb │ └── tank.rb ├── game_window.rb ├── main.rb └── states │ ├── game_state.rb │ ├── menu_state.rb │ └── play_state.rb ├── 04-profiling ├── naive_profile.txt ├── naive_profile_2.txt ├── naive_profile_3.txt ├── play_profile.txt ├── play_profile_2.txt └── play_profile_3.txt ├── 04-prototype-optimized ├── README.md ├── entities │ ├── bullet.rb │ ├── camera.rb │ ├── explosion.rb │ ├── map.rb │ └── tank.rb ├── game_window.rb ├── main.rb └── states │ ├── game_state.rb │ ├── menu_state.rb │ └── play_state.rb ├── 05-refactor ├── entities │ ├── bullet.rb │ ├── camera.rb │ ├── components │ │ ├── bullet_graphics.rb │ │ ├── bullet_physics.rb │ │ ├── bullet_sounds.rb │ │ ├── component.rb │ │ ├── explosion_graphics.rb │ │ ├── explosion_sounds.rb │ │ ├── player_input.rb │ │ ├── tank_graphics.rb │ │ ├── tank_physics.rb │ │ └── tank_sounds.rb │ ├── explosion.rb │ ├── game_object.rb │ ├── map.rb │ ├── object_pool.rb │ └── tank.rb ├── game_states │ ├── game_state.rb │ ├── menu_state.rb │ └── play_state.rb ├── game_window.rb ├── main.rb └── utils.rb ├── 06-physics ├── entities │ ├── bullet.rb │ ├── camera.rb │ ├── components │ │ ├── ai_input.rb │ │ ├── bullet_graphics.rb │ │ ├── bullet_physics.rb │ │ ├── bullet_sounds.rb │ │ ├── component.rb │ │ ├── explosion_graphics.rb │ │ ├── explosion_sounds.rb │ │ ├── player_input.rb │ │ ├── tank_graphics.rb │ │ ├── tank_physics.rb │ │ └── tank_sounds.rb │ ├── explosion.rb │ ├── game_object.rb │ ├── map.rb │ ├── object_pool.rb │ └── tank.rb ├── game_states │ ├── game_state.rb │ ├── menu_state.rb │ └── play_state.rb ├── game_window.rb ├── main.rb └── utils.rb ├── 07-damage ├── entities │ ├── bullet.rb │ ├── camera.rb │ ├── components │ │ ├── ai_input.rb │ │ ├── bullet_graphics.rb │ │ ├── bullet_physics.rb │ │ ├── bullet_sounds.rb │ │ ├── component.rb │ │ ├── explosion_graphics.rb │ │ ├── explosion_sounds.rb │ │ ├── player_input.rb │ │ ├── tank_graphics.rb │ │ ├── tank_health.rb │ │ ├── tank_physics.rb │ │ └── tank_sounds.rb │ ├── explosion.rb │ ├── game_object.rb │ ├── map.rb │ ├── object_pool.rb │ └── tank.rb ├── game_states │ ├── game_state.rb │ ├── menu_state.rb │ └── play_state.rb ├── game_window.rb ├── main.rb └── utils.rb ├── 08-ai ├── entities │ ├── bullet.rb │ ├── camera.rb │ ├── components │ │ ├── ai │ │ │ ├── gun.rb │ │ │ ├── tank_chasing_state.rb │ │ │ ├── tank_fighting_state.rb │ │ │ ├── tank_fleeing_state.rb │ │ │ ├── tank_motion_fsm.rb │ │ │ ├── tank_motion_state.rb │ │ │ ├── tank_roaming_state.rb │ │ │ └── vision.rb │ │ ├── ai_input.rb │ │ ├── bullet_graphics.rb │ │ ├── bullet_physics.rb │ │ ├── bullet_sounds.rb │ │ ├── component.rb │ │ ├── explosion_graphics.rb │ │ ├── explosion_sounds.rb │ │ ├── player_input.rb │ │ ├── tank_graphics.rb │ │ ├── tank_health.rb │ │ ├── tank_physics.rb │ │ └── tank_sounds.rb │ ├── explosion.rb │ ├── game_object.rb │ ├── map.rb │ ├── object_pool.rb │ └── tank.rb ├── game_states │ ├── game_state.rb │ ├── menu_state.rb │ └── play_state.rb ├── game_window.rb ├── main.rb └── utils.rb ├── 09-polishing ├── entities │ ├── box.rb │ ├── bullet.rb │ ├── camera.rb │ ├── components │ │ ├── ai │ │ │ ├── gun.rb │ │ │ ├── tank_chasing_state.rb │ │ │ ├── tank_fighting_state.rb │ │ │ ├── tank_fleeing_state.rb │ │ │ ├── tank_motion_fsm.rb │ │ │ ├── tank_motion_state.rb │ │ │ ├── tank_roaming_state.rb │ │ │ └── vision.rb │ │ ├── ai_input.rb │ │ ├── box_graphics.rb │ │ ├── bullet_graphics.rb │ │ ├── bullet_physics.rb │ │ ├── bullet_sounds.rb │ │ ├── component.rb │ │ ├── damage_graphics.rb │ │ ├── explosion_graphics.rb │ │ ├── explosion_sounds.rb │ │ ├── health.rb │ │ ├── player_input.rb │ │ ├── player_sounds.rb │ │ ├── tank_graphics.rb │ │ ├── tank_health.rb │ │ ├── tank_physics.rb │ │ ├── tank_sounds.rb │ │ └── tree_graphics.rb │ ├── damage.rb │ ├── explosion.rb │ ├── game_object.rb │ ├── map.rb │ ├── object_pool.rb │ ├── radar.rb │ ├── tank.rb │ └── tree.rb ├── game_states │ ├── game_state.rb │ ├── menu_state.rb │ └── play_state.rb ├── game_window.rb ├── main.rb ├── misc │ ├── names.rb │ └── stereo_sample.rb └── utils.rb ├── 10-partitioning ├── entities │ ├── box.rb │ ├── bullet.rb │ ├── camera.rb │ ├── components │ │ ├── ai │ │ │ ├── gun.rb │ │ │ ├── tank_chasing_state.rb │ │ │ ├── tank_fighting_state.rb │ │ │ ├── tank_fleeing_state.rb │ │ │ ├── tank_motion_fsm.rb │ │ │ ├── tank_motion_state.rb │ │ │ ├── tank_roaming_state.rb │ │ │ └── vision.rb │ │ ├── ai_input.rb │ │ ├── box_graphics.rb │ │ ├── bullet_graphics.rb │ │ ├── bullet_physics.rb │ │ ├── bullet_sounds.rb │ │ ├── component.rb │ │ ├── damage_graphics.rb │ │ ├── explosion_graphics.rb │ │ ├── explosion_sounds.rb │ │ ├── health.rb │ │ ├── player_input.rb │ │ ├── player_sounds.rb │ │ ├── tank_graphics.rb │ │ ├── tank_health.rb │ │ ├── tank_physics.rb │ │ ├── tank_sounds.rb │ │ └── tree_graphics.rb │ ├── damage.rb │ ├── explosion.rb │ ├── game_object.rb │ ├── map.rb │ ├── object_pool.rb │ ├── radar.rb │ ├── tank.rb │ └── tree.rb ├── game_states │ ├── game_state.rb │ ├── menu_state.rb │ └── play_state.rb ├── game_window.rb ├── main.rb ├── misc │ ├── axis_aligned_bounding_box.rb │ ├── names.rb │ ├── quad_tree.rb │ └── stereo_sample.rb ├── spec │ └── misc │ │ ├── aabb_spec.rb │ │ └── quad_tree_spec.rb └── utils.rb ├── 11-powerups ├── entities │ ├── box.rb │ ├── bullet.rb │ ├── camera.rb │ ├── components │ │ ├── ai │ │ │ ├── gun.rb │ │ │ ├── tank_chasing_state.rb │ │ │ ├── tank_fighting_state.rb │ │ │ ├── tank_fleeing_state.rb │ │ │ ├── tank_motion_fsm.rb │ │ │ ├── tank_motion_state.rb │ │ │ ├── tank_roaming_state.rb │ │ │ └── vision.rb │ │ ├── ai_input.rb │ │ ├── box_graphics.rb │ │ ├── bullet_graphics.rb │ │ ├── bullet_physics.rb │ │ ├── bullet_sounds.rb │ │ ├── component.rb │ │ ├── damage_graphics.rb │ │ ├── explosion_graphics.rb │ │ ├── explosion_sounds.rb │ │ ├── health.rb │ │ ├── player_input.rb │ │ ├── player_sounds.rb │ │ ├── powerup_graphics.rb │ │ ├── powerup_sounds.rb │ │ ├── tank_graphics.rb │ │ ├── tank_health.rb │ │ ├── tank_physics.rb │ │ ├── tank_sounds.rb │ │ └── tree_graphics.rb │ ├── damage.rb │ ├── explosion.rb │ ├── game_object.rb │ ├── map.rb │ ├── object_pool.rb │ ├── powerups │ │ ├── fire_rate_powerup.rb │ │ ├── health_powerup.rb │ │ ├── powerup.rb │ │ ├── powerup_respawn_queue.rb │ │ ├── repair_powerup.rb │ │ └── tank_speed_powerup.rb │ ├── radar.rb │ ├── tank.rb │ └── tree.rb ├── game_states │ ├── game_state.rb │ ├── menu_state.rb │ └── play_state.rb ├── game_window.rb ├── main.rb ├── misc │ ├── axis_aligned_bounding_box.rb │ ├── names.rb │ ├── quad_tree.rb │ └── stereo_sample.rb ├── spec │ └── misc │ │ ├── aabb_spec.rb │ │ └── quad_tree_spec.rb └── utils.rb ├── 12-stats ├── entities │ ├── box.rb │ ├── bullet.rb │ ├── camera.rb │ ├── components │ │ ├── ai │ │ │ ├── gun.rb │ │ │ ├── tank_chasing_state.rb │ │ │ ├── tank_fighting_state.rb │ │ │ ├── tank_fleeing_state.rb │ │ │ ├── tank_motion_fsm.rb │ │ │ ├── tank_motion_state.rb │ │ │ ├── tank_roaming_state.rb │ │ │ └── vision.rb │ │ ├── ai_input.rb │ │ ├── box_graphics.rb │ │ ├── bullet_graphics.rb │ │ ├── bullet_physics.rb │ │ ├── bullet_sounds.rb │ │ ├── component.rb │ │ ├── damage_graphics.rb │ │ ├── explosion_graphics.rb │ │ ├── explosion_sounds.rb │ │ ├── health.rb │ │ ├── player_input.rb │ │ ├── player_sounds.rb │ │ ├── powerup_graphics.rb │ │ ├── powerup_sounds.rb │ │ ├── tank_graphics.rb │ │ ├── tank_health.rb │ │ ├── tank_physics.rb │ │ ├── tank_sounds.rb │ │ └── tree_graphics.rb │ ├── damage.rb │ ├── explosion.rb │ ├── game_object.rb │ ├── hud.rb │ ├── map.rb │ ├── object_pool.rb │ ├── powerups │ │ ├── fire_rate_powerup.rb │ │ ├── health_powerup.rb │ │ ├── powerup.rb │ │ ├── powerup_respawn_queue.rb │ │ ├── repair_powerup.rb │ │ └── tank_speed_powerup.rb │ ├── radar.rb │ ├── score_display.rb │ ├── tank.rb │ └── tree.rb ├── game_states │ ├── game_state.rb │ ├── menu_state.rb │ ├── pause_state.rb │ └── play_state.rb ├── main.rb ├── misc │ ├── axis_aligned_bounding_box.rb │ ├── game_window.rb │ ├── names.rb │ ├── quad_tree.rb │ ├── stats.rb │ ├── stereo_sample.rb │ └── utils.rb └── spec │ └── misc │ ├── aabb_spec.rb │ └── quad_tree_spec.rb ├── 13-advanced-ai ├── entities │ ├── box.rb │ ├── bullet.rb │ ├── camera.rb │ ├── components │ │ ├── ai │ │ │ ├── gun.rb │ │ │ ├── tank_chasing_state.rb │ │ │ ├── tank_fighting_state.rb │ │ │ ├── tank_fleeing_state.rb │ │ │ ├── tank_motion_fsm.rb │ │ │ ├── tank_motion_state.rb │ │ │ ├── tank_navigating_state.rb │ │ │ ├── tank_roaming_state.rb │ │ │ ├── tank_stuck_state.rb │ │ │ └── vision.rb │ │ ├── ai_input.rb │ │ ├── box_graphics.rb │ │ ├── bullet_graphics.rb │ │ ├── bullet_physics.rb │ │ ├── bullet_sounds.rb │ │ ├── component.rb │ │ ├── damage_graphics.rb │ │ ├── explosion_graphics.rb │ │ ├── explosion_sounds.rb │ │ ├── health.rb │ │ ├── player_input.rb │ │ ├── player_sounds.rb │ │ ├── powerup_graphics.rb │ │ ├── powerup_sounds.rb │ │ ├── tank_graphics.rb │ │ ├── tank_health.rb │ │ ├── tank_physics.rb │ │ ├── tank_sounds.rb │ │ └── tree_graphics.rb │ ├── damage.rb │ ├── explosion.rb │ ├── game_object.rb │ ├── hud.rb │ ├── map.rb │ ├── object_pool.rb │ ├── powerups │ │ ├── fire_rate_powerup.rb │ │ ├── health_powerup.rb │ │ ├── powerup.rb │ │ ├── powerup_respawn_queue.rb │ │ ├── repair_powerup.rb │ │ └── tank_speed_powerup.rb │ ├── radar.rb │ ├── score_display.rb │ ├── tank.rb │ └── tree.rb ├── game_states │ ├── demo_state.rb │ ├── game_state.rb │ ├── menu_state.rb │ ├── pause_state.rb │ └── play_state.rb ├── main.rb ├── misc │ ├── axis_aligned_bounding_box.rb │ ├── game_window.rb │ ├── names.rb │ ├── quad_tree.rb │ ├── stats.rb │ ├── stereo_sample.rb │ └── utils.rb └── spec │ └── misc │ ├── aabb_spec.rb │ └── quad_tree_spec.rb ├── ATTRIBUTION.md ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── README.md └── media ├── armalite_rifle.ttf ├── boxes_barrels.json ├── boxes_barrels.png ├── bullet.png ├── c_dot.png ├── country_field.png ├── crash.ogg ├── damage1.png ├── damage2.png ├── damage3.png ├── damage4.png ├── decor.json ├── decor.png ├── decor.psd ├── explosion.mp3 ├── explosion.png ├── fire.mp3 ├── ground.json ├── ground.png ├── ground_units.json ├── ground_units.png ├── menu_music.mp3 ├── metal_interaction2.wav ├── names.txt ├── pickups.json ├── pickups.png ├── powerup.mp3 ├── respawn.wav ├── tank_driving.mp3 ├── top_secret.ttf ├── trees.png ├── trees_packed.json ├── trees_packed.png └── water.png /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.1.3 2 | -------------------------------------------------------------------------------- /01-hello/hello_movement.rb: -------------------------------------------------------------------------------- 1 | require 'gosu' 2 | 3 | class GameWindow < Gosu::Window 4 | def initialize(width=320, height=240, fullscreen=false) 5 | super 6 | self.caption = 'Hello Movement' 7 | @x = @y = 10 8 | @draws = 0 9 | @buttons_down = 0 10 | end 11 | 12 | def update 13 | @x -= 1 if button_down?(Gosu::KbLeft) 14 | @x += 1 if button_down?(Gosu::KbRight) 15 | @y -= 1 if button_down?(Gosu::KbUp) 16 | @y += 1 if button_down?(Gosu::KbDown) 17 | end 18 | 19 | def button_down(id) 20 | close if id == Gosu::KbEscape 21 | @buttons_down += 1 22 | end 23 | 24 | def button_up(id) 25 | @buttons_down -= 1 26 | end 27 | 28 | def needs_redraw? 29 | @draws == 0 || @buttons_down > 0 30 | end 31 | 32 | def draw 33 | @draws += 1 34 | @message = Gosu::Image.from_text( 35 | self, info, Gosu.default_font_name, 30) 36 | @message.draw(@x, @y, 0) 37 | end 38 | 39 | private 40 | 41 | def info 42 | "[x:#{@x};y:#{@y};draws:#{@draws}]" 43 | end 44 | end 45 | 46 | window = GameWindow.new 47 | window.show 48 | -------------------------------------------------------------------------------- /01-hello/hello_world.rb: -------------------------------------------------------------------------------- 1 | require 'gosu' 2 | 3 | class GameWindow < Gosu::Window 4 | def initialize(width=320, height=240, fullscreen=false) 5 | super 6 | self.caption = 'Hello' 7 | @message = Gosu::Image.from_text( 8 | self, 'Hello, World!', Gosu.default_font_name, 30) 9 | end 10 | 11 | def draw 12 | @message.draw(10, 10, 0) 13 | end 14 | end 15 | 16 | window = GameWindow.new 17 | window.show 18 | -------------------------------------------------------------------------------- /02-warmup/island.rb: -------------------------------------------------------------------------------- 1 | require 'gosu' 2 | require 'gosu_tiled' 3 | 4 | class GameWindow < Gosu::Window 5 | MAP_FILE = File.join(File.dirname( 6 | __FILE__), 'island.json') 7 | SPEED = 5 8 | 9 | def initialize 10 | super(640, 480, false) 11 | @map = Gosu::Tiled.load_json(self, MAP_FILE) 12 | @x = @y = 0 13 | @first_render = true 14 | end 15 | 16 | def button_down(id) 17 | close if id == Gosu::KbEscape 18 | end 19 | 20 | def update 21 | @x -= SPEED if button_down?(Gosu::KbLeft) 22 | @x += SPEED if button_down?(Gosu::KbRight) 23 | @y -= SPEED if button_down?(Gosu::KbUp) 24 | @y += SPEED if button_down?(Gosu::KbDown) 25 | self.caption = "#{Gosu.fps} FPS. Use arrow keys to pan" 26 | end 27 | 28 | def draw 29 | @first_render = false 30 | @map.draw(@x, @y) 31 | end 32 | 33 | def needs_redraw? 34 | [Gosu::KbLeft, 35 | Gosu::KbRight, 36 | Gosu::KbUp, 37 | Gosu::KbDown].each do |b| 38 | return true if button_down?(b) 39 | end 40 | @first_render 41 | end 42 | end 43 | 44 | GameWindow.new.show 45 | -------------------------------------------------------------------------------- /02-warmup/random_map.rb: -------------------------------------------------------------------------------- 1 | require 'gosu' 2 | require 'gosu_texture_packer' 3 | 4 | def media_path(file) 5 | File.join(File.dirname(File.dirname( 6 | __FILE__)), 'media', file) 7 | end 8 | 9 | class GameWindow < Gosu::Window 10 | WIDTH = 800 11 | HEIGHT = 600 12 | TILE_SIZE = 128 13 | 14 | def initialize 15 | super(WIDTH, HEIGHT, false) 16 | self.caption = 'Random Map' 17 | @tileset = Gosu::TexturePacker.load_json( 18 | self, media_path('ground.json'), :precise) 19 | @redraw = true 20 | end 21 | 22 | def button_down(id) 23 | close if id == Gosu::KbEscape 24 | @redraw = true if id == Gosu::KbSpace 25 | end 26 | 27 | def needs_redraw? 28 | @redraw 29 | end 30 | 31 | def draw 32 | @redraw = false 33 | (0..WIDTH / TILE_SIZE).each do |x| 34 | (0..HEIGHT / TILE_SIZE).each do |y| 35 | @tileset.frame( 36 | @tileset.frame_list.sample).draw( 37 | x * (TILE_SIZE), 38 | y * (TILE_SIZE), 39 | 0) 40 | end 41 | end 42 | end 43 | end 44 | 45 | window = GameWindow.new 46 | window.show 47 | -------------------------------------------------------------------------------- /02-warmup/tileset.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | class Tileset 3 | def initialize(window, json) 4 | @json = JSON.parse(File.read(json)) 5 | image_file = File.join( 6 | File.dirname(json), @json['meta']['image']) 7 | @main_image = Gosu::Image.new( 8 | @window, image_file, true) 9 | end 10 | 11 | def frame(name) 12 | f = @json['frames'][name]['frame'] 13 | @main_image.subimage( 14 | f['x'], f['y'], f['w'], f['h']) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /03-prototype/README.md: -------------------------------------------------------------------------------- 1 | # Tanks Game Prototype 2 | 3 | This is a prototype of Tanks game that was built while writing [Developing Games With Ruby](https://leanpub.com/developing-games-with-ruby) 4 | 5 | ## Features 6 | 7 | - Camera loosely follows tank 8 | - Camera zooms automatically depending on tank speed 9 | - Manual camera zoom override 10 | - Music and sound effects 11 | - Randomly generated map 12 | - Two modes: menu and gameplay 13 | - Tank movement with WADS keys 14 | - Tank aiming and shooting with mouse 15 | - Collision detection (tanks don't swim) 16 | - Explosions, visible bullet trajectories 17 | - Bullet range limiting 18 | 19 | ## Video 20 | 21 | [![Click to watch on YouTube](http://img.youtube.com/vi/ZP5y63JIXfc/0.jpg)](https://www.youtube.com/watch?v=ZP5y63JIXfc) 22 | -------------------------------------------------------------------------------- /03-prototype/entities/explosion.rb: -------------------------------------------------------------------------------- 1 | class Explosion 2 | FRAME_DELAY = 10 # ms 3 | 4 | def animation 5 | @@animation ||= 6 | Gosu::Image.load_tiles( 7 | $window, Game.media_path('explosion.png'), 128, 128, false) 8 | end 9 | 10 | def sound 11 | @@sound ||= Gosu::Sample.new( 12 | $window, Game.media_path('explosion.mp3')) 13 | end 14 | 15 | def initialize(x, y) 16 | sound.play 17 | @x, @y = x, y 18 | @current_frame = 0 19 | end 20 | 21 | def update 22 | @current_frame += 1 if frame_expired? 23 | end 24 | 25 | def draw 26 | return if done? 27 | image = current_frame 28 | image.draw( 29 | @x - image.width / 2 + 3, 30 | @y - image.height / 2 - 35, 31 | 20) 32 | end 33 | 34 | def done? 35 | @done ||= @current_frame == animation.size 36 | end 37 | 38 | private 39 | 40 | def current_frame 41 | animation[@current_frame % animation.size] 42 | end 43 | 44 | def frame_expired? 45 | now = Gosu.milliseconds 46 | @last_frame ||= now 47 | if (now - @last_frame) > FRAME_DELAY 48 | @last_frame = now 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /03-prototype/game_window.rb: -------------------------------------------------------------------------------- 1 | class GameWindow < Gosu::Window 2 | 3 | attr_accessor :state 4 | 5 | def initialize 6 | super(800, 600, false) 7 | end 8 | 9 | def update 10 | @state.update 11 | end 12 | 13 | def draw 14 | @state.draw 15 | end 16 | 17 | def needs_redraw? 18 | @state.needs_redraw? 19 | end 20 | 21 | def button_down(id) 22 | @state.button_down(id) 23 | end 24 | 25 | end 26 | -------------------------------------------------------------------------------- /03-prototype/main.rb: -------------------------------------------------------------------------------- 1 | require 'gosu' 2 | require_relative 'states/game_state' 3 | require_relative 'states/menu_state' 4 | require_relative 'states/play_state' 5 | require_relative 'game_window' 6 | 7 | module Game 8 | def self.media_path(file) 9 | File.join(File.dirname(File.dirname( 10 | __FILE__)), 'media', file) 11 | end 12 | end 13 | 14 | $window = GameWindow.new 15 | GameState.switch(MenuState.instance) 16 | $window.show 17 | -------------------------------------------------------------------------------- /03-prototype/states/game_state.rb: -------------------------------------------------------------------------------- 1 | class GameState 2 | 3 | def self.switch(new_state) 4 | $window.state && $window.state.leave 5 | $window.state = new_state 6 | new_state.enter 7 | end 8 | 9 | def enter 10 | end 11 | 12 | def leave 13 | end 14 | 15 | def draw 16 | end 17 | 18 | def update 19 | end 20 | 21 | def needs_redraw? 22 | true 23 | end 24 | 25 | def button_down(id) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /03-prototype/states/menu_state.rb: -------------------------------------------------------------------------------- 1 | require 'singleton' 2 | class MenuState < GameState 3 | include Singleton 4 | attr_accessor :play_state 5 | 6 | def initialize 7 | @message = Gosu::Image.from_text( 8 | $window, "Tanks Prototype", 9 | Gosu.default_font_name, 100) 10 | end 11 | 12 | def enter 13 | music.play(true) 14 | music.volume = 1 15 | end 16 | 17 | def leave 18 | music.volume = 0 19 | music.stop 20 | end 21 | 22 | def music 23 | @@music ||= Gosu::Song.new( 24 | $window, Game.media_path('menu_music.mp3')) 25 | end 26 | 27 | def update 28 | continue_text = @play_state ? "C = Continue, " : "" 29 | @info = Gosu::Image.from_text( 30 | $window, "Q = Quit, #{continue_text}N = New Game", 31 | Gosu.default_font_name, 30) 32 | end 33 | 34 | def draw 35 | @message.draw( 36 | $window.width / 2 - @message.width / 2, 37 | $window.height / 2 - @message.height / 2, 38 | 10) 39 | @info.draw( 40 | $window.width / 2 - @info.width / 2, 41 | $window.height / 2 - @info.height / 2 + 200, 42 | 10) 43 | end 44 | 45 | def button_down(id) 46 | $window.close if id == Gosu::KbQ 47 | if id == Gosu::KbC && @play_state 48 | GameState.switch(@play_state) 49 | end 50 | if id == Gosu::KbN 51 | @play_state = PlayState.new 52 | GameState.switch(@play_state) 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /04-prototype-optimized/README.md: -------------------------------------------------------------------------------- 1 | # Tanks Game Prototype 2 | 3 | This is a prototype of Tanks game that was built while writing [Developing Games With Ruby](https://leanpub.com/developing-games-with-ruby) 4 | 5 | ## Features 6 | 7 | - Camera loosely follows tank 8 | - Camera zooms automatically depending on tank speed 9 | - Manual camera zoom override 10 | - Music and sound effects 11 | - Randomly generated map 12 | - Two modes: menu and gameplay 13 | - Tank movement with WADS keys 14 | - Tank aiming and shooting with mouse 15 | - Collision detection (tanks don't swim) 16 | - Explosions, visible bullet trajectories 17 | - Bullet range limiting 18 | 19 | ## Video 20 | 21 | [![Click to watch on YouTube](http://img.youtube.com/vi/ZP5y63JIXfc/0.jpg)](https://www.youtube.com/watch?v=ZP5y63JIXfc) 22 | -------------------------------------------------------------------------------- /04-prototype-optimized/entities/explosion.rb: -------------------------------------------------------------------------------- 1 | class Explosion 2 | FRAME_DELAY = 16.66 # ms 3 | 4 | def animation 5 | @@animation ||= 6 | Gosu::Image.load_tiles( 7 | $window, Game.media_path('explosion.png'), 128, 128, false) 8 | end 9 | 10 | def sound 11 | @@sound ||= Gosu::Sample.new( 12 | $window, Game.media_path('explosion.mp3')) 13 | end 14 | 15 | def initialize(x, y) 16 | sound.play 17 | @x, @y = x, y 18 | @current_frame = 0 19 | end 20 | 21 | def update 22 | advance_frame 23 | end 24 | 25 | def draw 26 | return if done? 27 | image = current_frame 28 | image.draw( 29 | @x - image.width / 2 + 3, 30 | @y - image.height / 2 - 35, 31 | 20) 32 | end 33 | 34 | def done? 35 | @done ||= @current_frame >= animation.size 36 | end 37 | 38 | private 39 | 40 | def current_frame 41 | animation[@current_frame % animation.size] 42 | end 43 | 44 | def advance_frame 45 | now = Gosu.milliseconds 46 | delta = now - (@last_frame ||= now) 47 | if delta > FRAME_DELAY 48 | @last_frame = now 49 | end 50 | @current_frame += (delta / FRAME_DELAY).floor 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /04-prototype-optimized/game_window.rb: -------------------------------------------------------------------------------- 1 | class GameWindow < Gosu::Window 2 | attr_accessor :state 3 | 4 | def initialize 5 | super(800, 600, false) 6 | end 7 | 8 | def update 9 | Game.track_update_interval 10 | @state.update 11 | end 12 | 13 | def draw 14 | @state.draw 15 | end 16 | 17 | def needs_redraw? 18 | @state.needs_redraw? 19 | end 20 | 21 | def needs_cursor? 22 | Game.update_interval > 200 23 | end 24 | 25 | def button_down(id) 26 | @state.button_down(id) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /04-prototype-optimized/main.rb: -------------------------------------------------------------------------------- 1 | require 'gosu' 2 | require_relative 'states/game_state' 3 | require_relative 'states/menu_state' 4 | require_relative 'states/play_state' 5 | require_relative 'game_window' 6 | 7 | module Game 8 | def self.media_path(file) 9 | File.join(File.dirname(File.dirname( 10 | __FILE__)), 'media', file) 11 | end 12 | 13 | def self.track_update_interval 14 | now = Gosu.milliseconds 15 | @update_interval = (now - (@last_update ||= 0)).to_f 16 | @last_update = now 17 | end 18 | 19 | def self.update_interval 20 | @update_interval ||= $window.update_interval 21 | end 22 | 23 | def self.adjust_speed(speed) 24 | speed * update_interval / 33.33 25 | end 26 | end 27 | 28 | $window = GameWindow.new 29 | GameState.switch(MenuState.instance) 30 | $window.show 31 | -------------------------------------------------------------------------------- /04-prototype-optimized/states/game_state.rb: -------------------------------------------------------------------------------- 1 | class GameState 2 | 3 | def self.switch(new_state) 4 | $window.state && $window.state.leave 5 | $window.state = new_state 6 | new_state.enter 7 | end 8 | 9 | def enter 10 | end 11 | 12 | def leave 13 | end 14 | 15 | def draw 16 | end 17 | 18 | def update 19 | end 20 | 21 | def needs_redraw? 22 | true 23 | end 24 | 25 | def button_down(id) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /05-refactor/entities/bullet.rb: -------------------------------------------------------------------------------- 1 | class Bullet < GameObject 2 | attr_accessor :x, :y, :target_x, :target_y, :speed, :fired_at 3 | 4 | def initialize(object_pool, source_x, source_y, target_x, target_y) 5 | super(object_pool) 6 | @x, @y = source_x, source_y 7 | @target_x, @target_y = target_x, target_y 8 | BulletPhysics.new(self) 9 | BulletGraphics.new(self) 10 | BulletSounds.play 11 | end 12 | 13 | def explode 14 | Explosion.new(object_pool, @x, @y) 15 | mark_for_removal 16 | end 17 | 18 | def fire(speed) 19 | @speed = speed 20 | @fired_at = Gosu.milliseconds 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /05-refactor/entities/components/bullet_graphics.rb: -------------------------------------------------------------------------------- 1 | class BulletGraphics < Component 2 | COLOR = Gosu::Color::BLACK 3 | 4 | def draw(viewport) 5 | $window.draw_quad(x - 2, y - 2, COLOR, 6 | x + 2, y - 2, COLOR, 7 | x - 2, y + 2, COLOR, 8 | x + 2, y + 2, COLOR, 9 | 1) 10 | end 11 | 12 | end 13 | -------------------------------------------------------------------------------- /05-refactor/entities/components/bullet_physics.rb: -------------------------------------------------------------------------------- 1 | class BulletPhysics < Component 2 | START_DIST = 20 3 | MAX_DIST = 300 4 | 5 | def initialize(game_object) 6 | super 7 | object.x, object.y = point_at_distance(START_DIST) 8 | if trajectory_length > MAX_DIST 9 | object.target_x, object.target_y = point_at_distance(MAX_DIST) 10 | end 11 | end 12 | 13 | def update 14 | fly_speed = Utils.adjust_speed(object.speed) 15 | fly_distance = (Gosu.milliseconds - object.fired_at) * 0.001 * fly_speed 16 | object.x, object.y = point_at_distance(fly_distance) 17 | object.explode if arrived? 18 | end 19 | 20 | def trajectory_length 21 | d_x = object.target_x - x 22 | d_y = object.target_y - y 23 | Math.sqrt(d_x * d_x + d_y * d_y) 24 | end 25 | 26 | def point_at_distance(distance) 27 | if distance > trajectory_length 28 | return [object.target_x, object.target_y] 29 | end 30 | distance_factor = distance.to_f / trajectory_length 31 | p_x = x + (object.target_x - x) * distance_factor 32 | p_y = y + (object.target_y - y) * distance_factor 33 | [p_x, p_y] 34 | end 35 | 36 | private 37 | 38 | def arrived? 39 | x == object.target_x && y == object.target_y 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /05-refactor/entities/components/bullet_sounds.rb: -------------------------------------------------------------------------------- 1 | class BulletSounds 2 | class << self 3 | def play 4 | sound.play 5 | end 6 | 7 | private 8 | 9 | def sound 10 | @@sound ||= Gosu::Sample.new( 11 | $window, Utils.media_path('fire.mp3')) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /05-refactor/entities/components/component.rb: -------------------------------------------------------------------------------- 1 | class Component 2 | def initialize(game_object = nil) 3 | self.object = game_object 4 | end 5 | 6 | def update 7 | # override 8 | end 9 | 10 | def draw(viewport) 11 | # override 12 | end 13 | 14 | protected 15 | 16 | def object=(obj) 17 | if obj 18 | @object = obj 19 | obj.components << self 20 | end 21 | end 22 | 23 | def x 24 | @object.x 25 | end 26 | 27 | def y 28 | @object.y 29 | end 30 | 31 | def object 32 | @object 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /05-refactor/entities/components/explosion_graphics.rb: -------------------------------------------------------------------------------- 1 | class ExplosionGraphics < Component 2 | FRAME_DELAY = 16.66 # ms 3 | 4 | def initialize(game_object) 5 | super 6 | @current_frame = 0 7 | end 8 | 9 | def draw(viewport) 10 | image = current_frame 11 | image.draw( 12 | x - image.width / 2 + 3, 13 | y - image.height / 2 - 35, 14 | 20) 15 | end 16 | 17 | def update 18 | now = Gosu.milliseconds 19 | delta = now - (@last_frame ||= now) 20 | if delta > FRAME_DELAY 21 | @last_frame = now 22 | end 23 | @current_frame += (delta / FRAME_DELAY).floor 24 | object.mark_for_removal if done? 25 | end 26 | 27 | private 28 | 29 | def current_frame 30 | animation[@current_frame % animation.size] 31 | end 32 | 33 | def done? 34 | @done ||= @current_frame >= animation.size 35 | end 36 | 37 | def animation 38 | @@animation ||= 39 | Gosu::Image.load_tiles( 40 | $window, Utils.media_path('explosion.png'), 41 | 128, 128, false) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /05-refactor/entities/components/explosion_sounds.rb: -------------------------------------------------------------------------------- 1 | class ExplosionSounds 2 | class << self 3 | def play 4 | sound.play 5 | end 6 | 7 | private 8 | 9 | def sound 10 | @@sound ||= Gosu::Sample.new( 11 | $window, Utils.media_path('explosion.mp3')) 12 | end 13 | end 14 | end 15 | 16 | -------------------------------------------------------------------------------- /05-refactor/entities/components/tank_graphics.rb: -------------------------------------------------------------------------------- 1 | class TankGraphics < Component 2 | def initialize(game_object) 3 | super(game_object) 4 | @body = units.frame('tank1_body.png') 5 | @shadow = units.frame('tank1_body_shadow.png') 6 | @gun = units.frame('tank1_dualgun.png') 7 | end 8 | 9 | def draw(viewport) 10 | @shadow.draw_rot(x - 1, y - 1, 0, object.direction) 11 | @body.draw_rot(x, y, 1, object.direction) 12 | @gun.draw_rot(x, y, 2, object.gun_angle) 13 | end 14 | 15 | private 16 | 17 | def units 18 | @@units = Gosu::TexturePacker.load_json( 19 | $window, Utils.media_path('ground_units.json'), :precise) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /05-refactor/entities/components/tank_sounds.rb: -------------------------------------------------------------------------------- 1 | class TankSounds < Component 2 | def update 3 | if object.physics.moving? 4 | if @driving && @driving.paused? 5 | @driving.resume 6 | elsif @driving.nil? 7 | @driving = driving_sound.play(1, 1, true) 8 | end 9 | else 10 | if @driving && @driving.playing? 11 | @driving.pause 12 | end 13 | end 14 | end 15 | 16 | def collide 17 | crash_sound.play(1, 0.25, false) 18 | end 19 | 20 | private 21 | 22 | def driving_sound 23 | @@driving_sound ||= Gosu::Sample.new( 24 | $window, Utils.media_path('tank_driving.mp3')) 25 | end 26 | 27 | def crash_sound 28 | @@crash_sound ||= Gosu::Sample.new( 29 | $window, Utils.media_path('crash.ogg')) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /05-refactor/entities/explosion.rb: -------------------------------------------------------------------------------- 1 | class Explosion < GameObject 2 | attr_accessor :x, :y 3 | 4 | def initialize(object_pool, x, y) 5 | super(object_pool) 6 | @x, @y = x, y 7 | ExplosionGraphics.new(self) 8 | ExplosionSounds.play 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /05-refactor/entities/game_object.rb: -------------------------------------------------------------------------------- 1 | class GameObject 2 | def initialize(object_pool) 3 | @components = [] 4 | @object_pool = object_pool 5 | @object_pool.objects << self 6 | end 7 | 8 | def components 9 | @components 10 | end 11 | 12 | def update 13 | @components.map(&:update) 14 | end 15 | 16 | def draw(viewport) 17 | @components.each { |c| c.draw(viewport) } 18 | end 19 | 20 | def removable? 21 | @removable 22 | end 23 | 24 | def mark_for_removal 25 | @removable = true 26 | end 27 | 28 | protected 29 | 30 | def object_pool 31 | @object_pool 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /05-refactor/entities/object_pool.rb: -------------------------------------------------------------------------------- 1 | class ObjectPool 2 | attr_accessor :objects, :map 3 | def initialize(map) 4 | @map = map 5 | @objects = [] 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /05-refactor/entities/tank.rb: -------------------------------------------------------------------------------- 1 | class Tank < GameObject 2 | SHOOT_DELAY = 500 3 | attr_accessor :x, :y, :throttle_down, :direction, :gun_angle, :sounds, :physics 4 | 5 | def initialize(object_pool, input) 6 | super(object_pool) 7 | @input = input 8 | @input.control(self) 9 | @physics = TankPhysics.new(self, object_pool) 10 | @graphics = TankGraphics.new(self) 11 | @sounds = TankSounds.new(self) 12 | @direction = @gun_angle = 0.0 13 | end 14 | 15 | def shoot(target_x, target_y) 16 | if Gosu.milliseconds - (@last_shot || 0) > SHOOT_DELAY 17 | @last_shot = Gosu.milliseconds 18 | Bullet.new(object_pool, @x, @y, target_x, target_y).fire(100) 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /05-refactor/game_states/game_state.rb: -------------------------------------------------------------------------------- 1 | class GameState 2 | 3 | def self.switch(new_state) 4 | $window.state && $window.state.leave 5 | $window.state = new_state 6 | new_state.enter 7 | end 8 | 9 | def enter 10 | end 11 | 12 | def leave 13 | end 14 | 15 | def draw 16 | end 17 | 18 | def update 19 | end 20 | 21 | def needs_redraw? 22 | true 23 | end 24 | 25 | def button_down(id) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /05-refactor/game_window.rb: -------------------------------------------------------------------------------- 1 | class GameWindow < Gosu::Window 2 | attr_accessor :state 3 | 4 | def initialize 5 | super(800, 600, false) 6 | end 7 | 8 | def update 9 | Utils.track_update_interval 10 | @state.update 11 | end 12 | 13 | def draw 14 | @state.draw 15 | end 16 | 17 | def needs_redraw? 18 | @state.needs_redraw? 19 | end 20 | 21 | def needs_cursor? 22 | Utils.update_interval > 200 23 | end 24 | 25 | def button_down(id) 26 | @state.button_down(id) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /05-refactor/main.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'gosu' 4 | 5 | root_dir = File.dirname(__FILE__) 6 | require_pattern = File.join(root_dir, '**/*.rb') 7 | @failed = [] 8 | 9 | # Dynamically require everything 10 | Dir.glob(require_pattern).each do |f| 11 | next if f.end_with?('/main.rb') 12 | begin 13 | require_relative f.gsub("#{root_dir}/", '') 14 | rescue 15 | # May fail if parent class not required yet 16 | @failed << f 17 | end 18 | end 19 | 20 | # Retry unresolved requires 21 | @failed.each do |f| 22 | require_relative f.gsub("#{root_dir}/", '') 23 | end 24 | 25 | $window = GameWindow.new 26 | GameState.switch(MenuState.instance) 27 | $window.show 28 | -------------------------------------------------------------------------------- /05-refactor/utils.rb: -------------------------------------------------------------------------------- 1 | module Utils 2 | def self.media_path(file) 3 | File.join(File.dirname(File.dirname( 4 | __FILE__)), 'media', file) 5 | end 6 | 7 | def self.track_update_interval 8 | now = Gosu.milliseconds 9 | @update_interval = (now - (@last_update ||= 0)).to_f 10 | @last_update = now 11 | end 12 | 13 | def self.update_interval 14 | @update_interval ||= $window.update_interval 15 | end 16 | 17 | def self.adjust_speed(speed) 18 | speed * update_interval / 33.33 19 | end 20 | 21 | def self.button_down?(button) 22 | @buttons ||= {} 23 | now = Gosu.milliseconds 24 | now = now - (now % 150) 25 | if $window.button_down?(button) 26 | @buttons[button] = now 27 | true 28 | elsif @buttons[button] 29 | if now == @buttons[button] 30 | true 31 | else 32 | @buttons.delete(button) 33 | false 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /06-physics/entities/bullet.rb: -------------------------------------------------------------------------------- 1 | class Bullet < GameObject 2 | attr_accessor :x, :y, :target_x, :target_y, :source, :speed, :fired_at 3 | 4 | def initialize(object_pool, source_x, source_y, target_x, target_y) 5 | super(object_pool) 6 | @x, @y = source_x, source_y 7 | @target_x, @target_y = target_x, target_y 8 | BulletPhysics.new(self, object_pool) 9 | BulletGraphics.new(self) 10 | BulletSounds.play 11 | end 12 | 13 | def box 14 | [x, y] 15 | end 16 | 17 | def explode 18 | Explosion.new(object_pool, @x, @y) 19 | mark_for_removal 20 | end 21 | 22 | def fire(source, speed) 23 | @source = source 24 | @speed = speed 25 | @fired_at = Gosu.milliseconds 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /06-physics/entities/components/ai_input.rb: -------------------------------------------------------------------------------- 1 | class AiInput < Component 2 | def control(obj) 3 | self.object = obj 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /06-physics/entities/components/bullet_graphics.rb: -------------------------------------------------------------------------------- 1 | class BulletGraphics < Component 2 | COLOR = Gosu::Color::BLACK 3 | 4 | def draw(viewport) 5 | $window.draw_quad(x - 2, y - 2, COLOR, 6 | x + 2, y - 2, COLOR, 7 | x - 2, y + 2, COLOR, 8 | x + 2, y + 2, COLOR, 9 | 1) 10 | end 11 | 12 | end 13 | -------------------------------------------------------------------------------- /06-physics/entities/components/bullet_sounds.rb: -------------------------------------------------------------------------------- 1 | class BulletSounds 2 | class << self 3 | def play 4 | sound.play 5 | end 6 | 7 | private 8 | 9 | def sound 10 | @@sound ||= Gosu::Sample.new( 11 | $window, Utils.media_path('fire.mp3')) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /06-physics/entities/components/component.rb: -------------------------------------------------------------------------------- 1 | class Component 2 | def initialize(game_object = nil) 3 | self.object = game_object 4 | end 5 | 6 | def update 7 | # override 8 | end 9 | 10 | def draw(viewport) 11 | # override 12 | end 13 | 14 | protected 15 | 16 | def object=(obj) 17 | if obj 18 | @object = obj 19 | obj.components << self 20 | end 21 | end 22 | 23 | def x 24 | @object.x 25 | end 26 | 27 | def y 28 | @object.y 29 | end 30 | 31 | def object 32 | @object 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /06-physics/entities/components/explosion_graphics.rb: -------------------------------------------------------------------------------- 1 | class ExplosionGraphics < Component 2 | FRAME_DELAY = 16.66 # ms 3 | 4 | def initialize(game_object) 5 | super 6 | @current_frame = 0 7 | end 8 | 9 | def draw(viewport) 10 | image = current_frame 11 | image.draw( 12 | x - image.width / 2 + 3, 13 | y - image.height / 2 - 35, 14 | 20) 15 | end 16 | 17 | def update 18 | now = Gosu.milliseconds 19 | delta = now - (@last_frame ||= now) 20 | if delta > FRAME_DELAY 21 | @last_frame = now 22 | end 23 | @current_frame += (delta / FRAME_DELAY).floor 24 | object.mark_for_removal if done? 25 | end 26 | 27 | private 28 | 29 | def current_frame 30 | animation[@current_frame % animation.size] 31 | end 32 | 33 | def done? 34 | @done ||= @current_frame >= animation.size 35 | end 36 | 37 | def animation 38 | @@animation ||= 39 | Gosu::Image.load_tiles( 40 | $window, Utils.media_path('explosion.png'), 41 | 128, 128, false) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /06-physics/entities/components/explosion_sounds.rb: -------------------------------------------------------------------------------- 1 | class ExplosionSounds 2 | class << self 3 | def play 4 | sound.play 5 | end 6 | 7 | private 8 | 9 | def sound 10 | @@sound ||= Gosu::Sample.new( 11 | $window, Utils.media_path('explosion.mp3')) 12 | end 13 | end 14 | end 15 | 16 | -------------------------------------------------------------------------------- /06-physics/entities/components/tank_graphics.rb: -------------------------------------------------------------------------------- 1 | class TankGraphics < Component 2 | DEBUG_COLORS = [ 3 | Gosu::Color::RED, 4 | Gosu::Color::BLUE, 5 | Gosu::Color::YELLOW, 6 | Gosu::Color::WHITE 7 | ] 8 | 9 | def initialize(game_object) 10 | super(game_object) 11 | @body = units.frame('tank1_body.png') 12 | @shadow = units.frame('tank1_body_shadow.png') 13 | @gun = units.frame('tank1_dualgun.png') 14 | end 15 | 16 | def draw(viewport) 17 | @shadow.draw_rot(x - 1, y - 1, 0, object.direction) 18 | @body.draw_rot(x, y, 1, object.direction) 19 | @gun.draw_rot(x, y, 2, object.gun_angle) 20 | draw_bounding_box if $debug 21 | end 22 | 23 | def width 24 | @body.width 25 | end 26 | 27 | def height 28 | @body.height 29 | end 30 | 31 | def draw_bounding_box 32 | i = 0 33 | object.box.each_slice(2) do |x, y| 34 | color = DEBUG_COLORS[i] 35 | $window.draw_triangle( 36 | x - 3, y - 3, color, 37 | x, y, color, 38 | x + 3, y - 3, color, 39 | 100) 40 | i = (i + 1) % 4 41 | end 42 | end 43 | 44 | private 45 | 46 | def units 47 | @@units = Gosu::TexturePacker.load_json( 48 | $window, Utils.media_path('ground_units.json'), :precise) 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /06-physics/entities/components/tank_sounds.rb: -------------------------------------------------------------------------------- 1 | class TankSounds < Component 2 | def update 3 | if object.physics.moving? 4 | if @driving && @driving.paused? 5 | @driving.resume 6 | elsif @driving.nil? 7 | @driving = driving_sound.play(0.3, 1, true) 8 | end 9 | else 10 | if @driving && @driving.playing? 11 | @driving.pause 12 | end 13 | end 14 | end 15 | 16 | def collide 17 | crash_sound.play(0.3, 0.25, false) 18 | end 19 | 20 | private 21 | 22 | def driving_sound 23 | @@driving_sound ||= Gosu::Sample.new( 24 | $window, Utils.media_path('tank_driving.mp3')) 25 | end 26 | 27 | def crash_sound 28 | @@crash_sound ||= Gosu::Sample.new( 29 | $window, Utils.media_path('crash.ogg')) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /06-physics/entities/explosion.rb: -------------------------------------------------------------------------------- 1 | class Explosion < GameObject 2 | attr_accessor :x, :y 3 | 4 | def initialize(object_pool, x, y) 5 | super(object_pool) 6 | @x, @y = x, y 7 | ExplosionGraphics.new(self) 8 | ExplosionSounds.play 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /06-physics/entities/game_object.rb: -------------------------------------------------------------------------------- 1 | class GameObject 2 | def initialize(object_pool) 3 | @components = [] 4 | @object_pool = object_pool 5 | @object_pool.objects << self 6 | end 7 | 8 | def components 9 | @components 10 | end 11 | 12 | def update 13 | @components.map(&:update) 14 | end 15 | 16 | def draw(viewport) 17 | @components.each { |c| c.draw(viewport) } 18 | end 19 | 20 | def removable? 21 | @removable 22 | end 23 | 24 | def mark_for_removal 25 | @removable = true 26 | end 27 | 28 | def box 29 | end 30 | 31 | def collide 32 | end 33 | 34 | protected 35 | 36 | def object_pool 37 | @object_pool 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /06-physics/entities/object_pool.rb: -------------------------------------------------------------------------------- 1 | class ObjectPool 2 | attr_accessor :objects, :map 3 | def initialize(map) 4 | @map = map 5 | @objects = [] 6 | end 7 | 8 | def nearby(object, max_distance) 9 | @objects.select do |obj| 10 | distance = Utils.distance_between( 11 | obj.x, obj.y, object.x, object.y) 12 | obj != object && distance < max_distance 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /06-physics/entities/tank.rb: -------------------------------------------------------------------------------- 1 | class Tank < GameObject 2 | SHOOT_DELAY = 500 3 | attr_accessor :x, :y, :throttle_down, :direction, :gun_angle, :sounds, :physics, :graphics 4 | 5 | def initialize(object_pool, input) 6 | super(object_pool) 7 | @input = input 8 | @input.control(self) 9 | @physics = TankPhysics.new(self, object_pool) 10 | @graphics = TankGraphics.new(self) 11 | @sounds = TankSounds.new(self) 12 | @direction = rand(0..7) * 45 13 | @gun_angle = rand(0..360) 14 | end 15 | 16 | def box 17 | @physics.box 18 | end 19 | 20 | def shoot(target_x, target_y) 21 | if Gosu.milliseconds - (@last_shot || 0) > SHOOT_DELAY 22 | @last_shot = Gosu.milliseconds 23 | Bullet.new(object_pool, @x, @y, target_x, target_y).fire(self, 100) 24 | end 25 | end 26 | 27 | end 28 | -------------------------------------------------------------------------------- /06-physics/game_states/game_state.rb: -------------------------------------------------------------------------------- 1 | class GameState 2 | 3 | def self.switch(new_state) 4 | $window.state && $window.state.leave 5 | $window.state = new_state 6 | new_state.enter 7 | end 8 | 9 | def enter 10 | end 11 | 12 | def leave 13 | end 14 | 15 | def draw 16 | end 17 | 18 | def update 19 | end 20 | 21 | def needs_redraw? 22 | true 23 | end 24 | 25 | def button_down(id) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /06-physics/game_window.rb: -------------------------------------------------------------------------------- 1 | class GameWindow < Gosu::Window 2 | attr_accessor :state 3 | 4 | def initialize 5 | super(800, 600, false) 6 | end 7 | 8 | def update 9 | Utils.track_update_interval 10 | @state.update 11 | end 12 | 13 | def draw 14 | @state.draw 15 | end 16 | 17 | def needs_redraw? 18 | @state.needs_redraw? 19 | end 20 | 21 | def needs_cursor? 22 | Utils.update_interval > 200 23 | end 24 | 25 | def button_down(id) 26 | @state.button_down(id) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /06-physics/main.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'gosu' 4 | 5 | root_dir = File.dirname(__FILE__) 6 | require_pattern = File.join(root_dir, '**/*.rb') 7 | @failed = [] 8 | 9 | # Dynamically require everything 10 | Dir.glob(require_pattern).each do |f| 11 | next if f.end_with?('/main.rb') 12 | begin 13 | require_relative f.gsub("#{root_dir}/", '') 14 | rescue 15 | # May fail if parent class not required yet 16 | @failed << f 17 | end 18 | end 19 | 20 | # Retry unresolved requires 21 | @failed.each do |f| 22 | require_relative f.gsub("#{root_dir}/", '') 23 | end 24 | 25 | $debug = false 26 | $window = GameWindow.new 27 | GameState.switch(MenuState.instance) 28 | $window.show 29 | -------------------------------------------------------------------------------- /07-damage/entities/bullet.rb: -------------------------------------------------------------------------------- 1 | class Bullet < GameObject 2 | attr_accessor :x, :y, :target_x, :target_y, :source, :speed, :fired_at 3 | 4 | def initialize(object_pool, source_x, source_y, target_x, target_y) 5 | super(object_pool) 6 | @x, @y = source_x, source_y 7 | @target_x, @target_y = target_x, target_y 8 | BulletPhysics.new(self, object_pool) 9 | BulletGraphics.new(self) 10 | BulletSounds.play 11 | end 12 | 13 | def box 14 | [x, y] 15 | end 16 | 17 | def explode 18 | Explosion.new(object_pool, @x, @y) 19 | mark_for_removal 20 | end 21 | 22 | def fire(source, speed) 23 | @source = source 24 | @speed = speed 25 | @fired_at = Gosu.milliseconds 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /07-damage/entities/components/ai_input.rb: -------------------------------------------------------------------------------- 1 | class AiInput < Component 2 | def control(obj) 3 | self.object = obj 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /07-damage/entities/components/bullet_graphics.rb: -------------------------------------------------------------------------------- 1 | class BulletGraphics < Component 2 | COLOR = Gosu::Color::BLACK 3 | 4 | def draw(viewport) 5 | $window.draw_quad(x - 2, y - 2, COLOR, 6 | x + 2, y - 2, COLOR, 7 | x - 2, y + 2, COLOR, 8 | x + 2, y + 2, COLOR, 9 | 1) 10 | end 11 | 12 | end 13 | -------------------------------------------------------------------------------- /07-damage/entities/components/bullet_sounds.rb: -------------------------------------------------------------------------------- 1 | class BulletSounds 2 | class << self 3 | def play 4 | sound.play 5 | end 6 | 7 | private 8 | 9 | def sound 10 | @@sound ||= Gosu::Sample.new( 11 | $window, Utils.media_path('fire.mp3')) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /07-damage/entities/components/component.rb: -------------------------------------------------------------------------------- 1 | class Component 2 | attr_reader :object # better performance 3 | 4 | def initialize(game_object = nil) 5 | self.object = game_object 6 | end 7 | 8 | def update 9 | # override 10 | end 11 | 12 | def draw(viewport) 13 | # override 14 | end 15 | 16 | protected 17 | 18 | def object=(obj) 19 | if obj 20 | @object = obj 21 | obj.components << self 22 | end 23 | end 24 | 25 | def x 26 | @object.x 27 | end 28 | 29 | def y 30 | @object.y 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /07-damage/entities/components/explosion_graphics.rb: -------------------------------------------------------------------------------- 1 | class ExplosionGraphics < Component 2 | FRAME_DELAY = 16.66 # ms 3 | 4 | def initialize(game_object) 5 | super 6 | @current_frame = 0 7 | end 8 | 9 | def draw(viewport) 10 | image = current_frame 11 | image.draw( 12 | x - image.width / 2 + 3, 13 | y - image.height / 2 - 35, 14 | 20) 15 | end 16 | 17 | def update 18 | now = Gosu.milliseconds 19 | delta = now - (@last_frame ||= now) 20 | if delta > FRAME_DELAY 21 | @last_frame = now 22 | end 23 | @current_frame += (delta / FRAME_DELAY).floor 24 | object.mark_for_removal if done? 25 | end 26 | 27 | private 28 | 29 | def current_frame 30 | animation[@current_frame % animation.size] 31 | end 32 | 33 | def done? 34 | @done ||= @current_frame >= animation.size 35 | end 36 | 37 | def animation 38 | @@animation ||= 39 | Gosu::Image.load_tiles( 40 | $window, Utils.media_path('explosion.png'), 41 | 128, 128, false) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /07-damage/entities/components/explosion_sounds.rb: -------------------------------------------------------------------------------- 1 | class ExplosionSounds 2 | class << self 3 | def play 4 | sound.play 5 | end 6 | 7 | private 8 | 9 | def sound 10 | @@sound ||= Gosu::Sample.new( 11 | $window, Utils.media_path('explosion.mp3')) 12 | end 13 | end 14 | end 15 | 16 | -------------------------------------------------------------------------------- /07-damage/entities/components/tank_health.rb: -------------------------------------------------------------------------------- 1 | class TankHealth < Component 2 | attr_accessor :health 3 | 4 | def initialize(object, object_pool) 5 | super(object) 6 | @object_pool = object_pool 7 | @health = 100 8 | @health_updated = true 9 | @last_damage = Gosu.milliseconds 10 | end 11 | 12 | def update 13 | update_image 14 | end 15 | 16 | def update_image 17 | if @health_updated 18 | if dead? 19 | text = '✝' 20 | font_size = 25 21 | else 22 | text = @health.to_s 23 | font_size = 18 24 | end 25 | @image = Gosu::Image.from_text( 26 | $window, text, 27 | Gosu.default_font_name, font_size) 28 | @health_updated = false 29 | end 30 | end 31 | 32 | def dead? 33 | @health < 1 34 | end 35 | 36 | def inflict_damage(amount) 37 | if @health > 0 38 | @health_updated = true 39 | @health = [@health - amount.to_i, 0].max 40 | if @health < 1 41 | Explosion.new(@object_pool, x, y) 42 | end 43 | end 44 | end 45 | 46 | def draw(viewport) 47 | @image.draw( 48 | x - @image.width / 2, 49 | y - object.graphics.height / 2 - 50 | @image.height, 100) 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /07-damage/entities/components/tank_sounds.rb: -------------------------------------------------------------------------------- 1 | class TankSounds < Component 2 | def update 3 | if object.physics.moving? 4 | if @driving && @driving.paused? 5 | @driving.resume 6 | elsif @driving.nil? 7 | @driving = driving_sound.play(0.1, 1, true) 8 | end 9 | else 10 | if @driving && @driving.playing? 11 | @driving.pause 12 | end 13 | end 14 | end 15 | 16 | def collide 17 | crash_sound.play(0.3, 0.25, false) 18 | end 19 | 20 | private 21 | 22 | def driving_sound 23 | @@driving_sound ||= Gosu::Sample.new( 24 | $window, Utils.media_path('tank_driving.mp3')) 25 | end 26 | 27 | def crash_sound 28 | @@crash_sound ||= Gosu::Sample.new( 29 | $window, Utils.media_path('crash.ogg')) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /07-damage/entities/explosion.rb: -------------------------------------------------------------------------------- 1 | class Explosion < GameObject 2 | attr_accessor :x, :y 3 | 4 | def initialize(object_pool, x, y) 5 | super(object_pool) 6 | @x, @y = x, y 7 | ExplosionGraphics.new(self) 8 | ExplosionSounds.play 9 | inflict_damage 10 | end 11 | 12 | private 13 | 14 | def inflict_damage 15 | object_pool.nearby(self, 100).each do |obj| 16 | if obj.class == Tank 17 | obj.health.inflict_damage( 18 | Math.sqrt(3 * 100 - Utils.distance_between( 19 | obj.x, obj.y, x, y))) 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /07-damage/entities/game_object.rb: -------------------------------------------------------------------------------- 1 | class GameObject 2 | def initialize(object_pool) 3 | @components = [] 4 | @object_pool = object_pool 5 | @object_pool.objects << self 6 | end 7 | 8 | def components 9 | @components 10 | end 11 | 12 | def update 13 | @components.map(&:update) 14 | end 15 | 16 | def draw(viewport) 17 | @components.each { |c| c.draw(viewport) } 18 | end 19 | 20 | def removable? 21 | @removable 22 | end 23 | 24 | def mark_for_removal 25 | @removable = true 26 | end 27 | 28 | def box 29 | end 30 | 31 | def collide 32 | end 33 | 34 | protected 35 | 36 | def object_pool 37 | @object_pool 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /07-damage/entities/object_pool.rb: -------------------------------------------------------------------------------- 1 | class ObjectPool 2 | attr_accessor :objects, :map 3 | def initialize(map) 4 | @map = map 5 | @objects = [] 6 | end 7 | 8 | def nearby(object, max_distance) 9 | @objects.select do |obj| 10 | distance = Utils.distance_between( 11 | obj.x, obj.y, object.x, object.y) 12 | obj != object && distance < max_distance 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /07-damage/entities/tank.rb: -------------------------------------------------------------------------------- 1 | class Tank < GameObject 2 | SHOOT_DELAY = 500 3 | attr_accessor :x, :y, :throttle_down, :direction, 4 | :gun_angle, :sounds, :physics, :graphics, :health 5 | 6 | def initialize(object_pool, input) 7 | super(object_pool) 8 | @input = input 9 | @input.control(self) 10 | @physics = TankPhysics.new(self, object_pool) 11 | @sounds = TankSounds.new(self) 12 | @health = TankHealth.new(self, object_pool) 13 | @graphics = TankGraphics.new(self) 14 | @direction = rand(0..7) * 45 15 | @gun_angle = rand(0..360) 16 | end 17 | 18 | def box 19 | @physics.box 20 | end 21 | 22 | def shoot(target_x, target_y) 23 | if Gosu.milliseconds - (@last_shot || 0) > SHOOT_DELAY 24 | @last_shot = Gosu.milliseconds 25 | Bullet.new(object_pool, @x, @y, target_x, target_y).fire(self, 100) 26 | end 27 | end 28 | 29 | end 30 | -------------------------------------------------------------------------------- /07-damage/game_states/game_state.rb: -------------------------------------------------------------------------------- 1 | class GameState 2 | 3 | def self.switch(new_state) 4 | $window.state && $window.state.leave 5 | $window.state = new_state 6 | new_state.enter 7 | end 8 | 9 | def enter 10 | end 11 | 12 | def leave 13 | end 14 | 15 | def draw 16 | end 17 | 18 | def update 19 | end 20 | 21 | def needs_redraw? 22 | true 23 | end 24 | 25 | def button_down(id) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /07-damage/game_window.rb: -------------------------------------------------------------------------------- 1 | class GameWindow < Gosu::Window 2 | attr_accessor :state 3 | 4 | def initialize 5 | super(800, 600, false) 6 | end 7 | 8 | def update 9 | Utils.track_update_interval 10 | @state.update 11 | end 12 | 13 | def draw 14 | @state.draw 15 | end 16 | 17 | def needs_redraw? 18 | @state.needs_redraw? 19 | end 20 | 21 | def needs_cursor? 22 | Utils.update_interval > 200 23 | end 24 | 25 | def button_down(id) 26 | @state.button_down(id) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /07-damage/main.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'gosu' 4 | 5 | root_dir = File.dirname(__FILE__) 6 | require_pattern = File.join(root_dir, '**/*.rb') 7 | @failed = [] 8 | 9 | # Dynamically require everything 10 | Dir.glob(require_pattern).each do |f| 11 | next if f.end_with?('/main.rb') 12 | begin 13 | require_relative f.gsub("#{root_dir}/", '') 14 | rescue 15 | # May fail if parent class not required yet 16 | @failed << f 17 | end 18 | end 19 | 20 | # Retry unresolved requires 21 | @failed.each do |f| 22 | require_relative f.gsub("#{root_dir}/", '') 23 | end 24 | 25 | $debug = false 26 | $window = GameWindow.new 27 | GameState.switch(MenuState.instance) 28 | $window.show 29 | -------------------------------------------------------------------------------- /08-ai/entities/bullet.rb: -------------------------------------------------------------------------------- 1 | class Bullet < GameObject 2 | attr_accessor :x, :y, :target_x, :target_y, :source, :speed, :fired_at 3 | 4 | def initialize(object_pool, source_x, source_y, target_x, target_y) 5 | super(object_pool) 6 | @x, @y = source_x, source_y 7 | @target_x, @target_y = target_x, target_y 8 | BulletPhysics.new(self, object_pool) 9 | BulletGraphics.new(self) 10 | BulletSounds.play 11 | end 12 | 13 | def box 14 | [x, y] 15 | end 16 | 17 | def explode 18 | Explosion.new(object_pool, @x, @y) 19 | mark_for_removal 20 | end 21 | 22 | def fire(source, speed) 23 | @source = source 24 | @speed = speed 25 | @fired_at = Gosu.milliseconds 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /08-ai/entities/components/ai/tank_chasing_state.rb: -------------------------------------------------------------------------------- 1 | class TankChasingState < TankMotionState 2 | def initialize(object, vision, gun) 3 | super(object, vision) 4 | @object = object 5 | @vision = vision 6 | @gun = gun 7 | end 8 | 9 | def update 10 | change_direction if should_change_direction? 11 | drive 12 | end 13 | 14 | def change_direction 15 | @object.physics.change_direction( 16 | @gun.desired_gun_angle - 17 | @gun.desired_gun_angle % 45) 18 | 19 | @changed_direction_at = Gosu.milliseconds 20 | @will_keep_direction_for = turn_time 21 | end 22 | 23 | def drive_time 24 | 10000 25 | end 26 | 27 | def turn_time 28 | rand(300..600) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /08-ai/entities/components/ai/tank_fighting_state.rb: -------------------------------------------------------------------------------- 1 | class TankFightingState < TankMotionState 2 | def initialize(object, vision) 3 | super 4 | @object = object 5 | @vision = vision 6 | end 7 | 8 | def update 9 | change_direction if should_change_direction? 10 | if substate_expired? 11 | rand > 0.2 ? drive : wait 12 | end 13 | end 14 | 15 | def change_direction 16 | change = case rand(0..100) 17 | when 0..20 18 | -45 19 | when 20..40 20 | 45 21 | when 40..60 22 | 90 23 | when 60..80 24 | -90 25 | when 80..90 26 | 135 27 | when 90..100 28 | -135 29 | end 30 | @object.physics.change_direction( 31 | @object.direction + change) 32 | @changed_direction_at = Gosu.milliseconds 33 | @will_keep_direction_for = turn_time 34 | end 35 | 36 | def wait_time 37 | rand(300..1000) 38 | end 39 | 40 | def drive_time 41 | rand(2000..5000) 42 | end 43 | 44 | def turn_time 45 | rand(500..2500) 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /08-ai/entities/components/ai/tank_fleeing_state.rb: -------------------------------------------------------------------------------- 1 | class TankFleeingState < TankMotionState 2 | MAX_FLEE_TIME = 15 * 1000 # 15 seconds 3 | def initialize(object, vision, gun) 4 | super(object, vision) 5 | @object = object 6 | @vision = vision 7 | @gun = gun 8 | end 9 | 10 | def can_flee? 11 | return true unless @started_fleeing 12 | Gosu.milliseconds - @started_fleeing < MAX_FLEE_TIME 13 | end 14 | 15 | def enter 16 | @started_fleeing ||= Gosu.milliseconds 17 | end 18 | 19 | def update 20 | change_direction if should_change_direction? 21 | drive 22 | end 23 | 24 | def change_direction 25 | @object.physics.change_direction( 26 | 180 + @gun.desired_gun_angle - 27 | @gun.desired_gun_angle % 45) 28 | 29 | @changed_direction_at = Gosu.milliseconds 30 | @will_keep_direction_for = turn_time 31 | end 32 | 33 | def drive_time 34 | 10000 35 | end 36 | 37 | def turn_time 38 | rand(300..600) 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /08-ai/entities/components/ai/tank_roaming_state.rb: -------------------------------------------------------------------------------- 1 | class TankRoamingState < TankMotionState 2 | def initialize(object, vision) 3 | super 4 | @object = object 5 | @vision = vision 6 | end 7 | 8 | def update 9 | change_direction if should_change_direction? 10 | if substate_expired? 11 | rand > 0.3 ? drive : wait 12 | end 13 | end 14 | 15 | def change_direction 16 | change = case rand(0..100) 17 | when 0..30 18 | -45 19 | when 30..60 20 | 45 21 | when 60..70 22 | 90 23 | when 80..90 24 | -90 25 | else 26 | 0 27 | end 28 | if change != 0 29 | @object.physics.change_direction( 30 | @object.direction + change) 31 | end 32 | @changed_direction_at = Gosu.milliseconds 33 | @will_keep_direction_for = turn_time 34 | end 35 | 36 | def wait_time 37 | rand(500..2000) 38 | end 39 | 40 | def drive_time 41 | rand(1000..5000) 42 | end 43 | 44 | def turn_time 45 | rand(2000..5000) 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /08-ai/entities/components/ai/vision.rb: -------------------------------------------------------------------------------- 1 | class AiVision 2 | CACHE_TIMEOUT = 500 3 | attr_reader :in_sight 4 | 5 | def initialize(viewer, object_pool, distance) 6 | @viewer = viewer 7 | @object_pool = object_pool 8 | @distance = distance 9 | end 10 | 11 | def update 12 | @in_sight = @object_pool.nearby(@viewer, @distance) 13 | end 14 | 15 | def closest_tank 16 | now = Gosu.milliseconds 17 | @closest_tank = nil 18 | if now - (@cache_updated_at ||= 0) > CACHE_TIMEOUT 19 | @closest_tank = nil 20 | @cache_updated_at = now 21 | end 22 | @closest_tank ||= find_closest_tank 23 | end 24 | 25 | private 26 | 27 | def find_closest_tank 28 | @in_sight.select do |o| 29 | o.class == Tank && !o.health.dead? 30 | end.sort do |a, b| 31 | x, y = @viewer.x, @viewer.y 32 | d1 = Utils.distance_between(x, y, a.x, a.y) 33 | d2 = Utils.distance_between(x, y, b.x, b.y) 34 | d1 <=> d2 35 | end.first 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /08-ai/entities/components/ai_input.rb: -------------------------------------------------------------------------------- 1 | class AiInput < Component 2 | UPDATE_RATE = 200 # ms 3 | 4 | def initialize(object_pool) 5 | @object_pool = object_pool 6 | super(nil) 7 | @last_update = Gosu.milliseconds 8 | end 9 | 10 | def control(obj) 11 | self.object = obj 12 | @vision = AiVision.new(obj, @object_pool, 13 | rand(700..1200)) 14 | @gun = AiGun.new(obj, @vision) 15 | @motion = TankMotionFSM.new(obj, @vision, @gun) 16 | end 17 | 18 | def on_collision(with) 19 | @motion.on_collision(with) 20 | end 21 | 22 | def on_damage(amount) 23 | @motion.on_damage(amount) 24 | end 25 | 26 | def update 27 | return if object.health.dead? 28 | @gun.adjust_angle 29 | now = Gosu.milliseconds 30 | return if now - @last_update < UPDATE_RATE 31 | @last_update = now 32 | @vision.update 33 | @gun.update 34 | @motion.update 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /08-ai/entities/components/bullet_graphics.rb: -------------------------------------------------------------------------------- 1 | class BulletGraphics < Component 2 | COLOR = Gosu::Color::BLACK 3 | 4 | def draw(viewport) 5 | $window.draw_quad(x - 2, y - 2, COLOR, 6 | x + 2, y - 2, COLOR, 7 | x - 2, y + 2, COLOR, 8 | x + 2, y + 2, COLOR, 9 | 1) 10 | end 11 | 12 | end 13 | -------------------------------------------------------------------------------- /08-ai/entities/components/bullet_sounds.rb: -------------------------------------------------------------------------------- 1 | class BulletSounds 2 | class << self 3 | def play 4 | sound.play 5 | end 6 | 7 | private 8 | 9 | def sound 10 | @@sound ||= Gosu::Sample.new( 11 | $window, Utils.media_path('fire.mp3')) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /08-ai/entities/components/component.rb: -------------------------------------------------------------------------------- 1 | class Component 2 | attr_reader :object # better performance 3 | 4 | def initialize(game_object = nil) 5 | self.object = game_object 6 | end 7 | 8 | def update 9 | # override 10 | end 11 | 12 | def draw(viewport) 13 | # override 14 | end 15 | 16 | protected 17 | 18 | def object=(obj) 19 | if obj 20 | @object = obj 21 | obj.components << self 22 | end 23 | end 24 | 25 | def x 26 | @object.x 27 | end 28 | 29 | def y 30 | @object.y 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /08-ai/entities/components/explosion_graphics.rb: -------------------------------------------------------------------------------- 1 | class ExplosionGraphics < Component 2 | FRAME_DELAY = 16.66 # ms 3 | 4 | def initialize(game_object) 5 | super 6 | @current_frame = 0 7 | end 8 | 9 | def draw(viewport) 10 | image = current_frame 11 | image.draw( 12 | x - image.width / 2 + 3, 13 | y - image.height / 2 - 35, 14 | 20) 15 | end 16 | 17 | def update 18 | now = Gosu.milliseconds 19 | delta = now - (@last_frame ||= now) 20 | if delta > FRAME_DELAY 21 | @last_frame = now 22 | end 23 | @current_frame += (delta / FRAME_DELAY).floor 24 | object.mark_for_removal if done? 25 | end 26 | 27 | private 28 | 29 | def current_frame 30 | animation[@current_frame % animation.size] 31 | end 32 | 33 | def done? 34 | @done ||= @current_frame >= animation.size 35 | end 36 | 37 | def animation 38 | @@animation ||= 39 | Gosu::Image.load_tiles( 40 | $window, Utils.media_path('explosion.png'), 41 | 128, 128, false) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /08-ai/entities/components/explosion_sounds.rb: -------------------------------------------------------------------------------- 1 | class ExplosionSounds 2 | class << self 3 | def play 4 | sound.play 5 | end 6 | 7 | private 8 | 9 | def sound 10 | @@sound ||= Gosu::Sample.new( 11 | $window, Utils.media_path('explosion.mp3')) 12 | end 13 | end 14 | end 15 | 16 | -------------------------------------------------------------------------------- /08-ai/entities/components/tank_health.rb: -------------------------------------------------------------------------------- 1 | class TankHealth < Component 2 | attr_accessor :health 3 | 4 | def initialize(object, object_pool) 5 | super(object) 6 | @object_pool = object_pool 7 | @health = 100 8 | @health_updated = true 9 | @last_damage = Gosu.milliseconds 10 | end 11 | 12 | def update 13 | update_image 14 | end 15 | 16 | def update_image 17 | if @health_updated 18 | if dead? 19 | text = '✝' 20 | font_size = 25 21 | else 22 | text = @health.to_s 23 | font_size = 18 24 | end 25 | @image = Gosu::Image.from_text( 26 | $window, text, 27 | Gosu.default_font_name, font_size) 28 | @health_updated = false 29 | end 30 | end 31 | 32 | def dead? 33 | @health < 1 34 | end 35 | 36 | def inflict_damage(amount) 37 | if @health > 0 38 | @health_updated = true 39 | @health = [@health - amount.to_i, 0].max 40 | object.input.on_damage(amount) 41 | if @health < 1 42 | Explosion.new(@object_pool, x, y) 43 | end 44 | end 45 | end 46 | 47 | def draw(viewport) 48 | @image && @image.draw( 49 | x - @image.width / 2, 50 | y - object.graphics.height / 2 - 51 | @image.height, 100) 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /08-ai/entities/components/tank_sounds.rb: -------------------------------------------------------------------------------- 1 | class TankSounds < Component 2 | def update 3 | if object.physics.moving? 4 | if @driving && @driving.paused? 5 | @driving.resume 6 | elsif @driving.nil? 7 | @driving = driving_sound.play(0.1, 1, true) 8 | end 9 | else 10 | if @driving && @driving.playing? 11 | @driving.pause 12 | end 13 | end 14 | end 15 | 16 | def collide 17 | crash_sound.play(0.3, 0.25, false) 18 | end 19 | 20 | private 21 | 22 | def driving_sound 23 | @@driving_sound ||= Gosu::Sample.new( 24 | $window, Utils.media_path('tank_driving.mp3')) 25 | end 26 | 27 | def crash_sound 28 | @@crash_sound ||= Gosu::Sample.new( 29 | $window, Utils.media_path('crash.ogg')) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /08-ai/entities/explosion.rb: -------------------------------------------------------------------------------- 1 | class Explosion < GameObject 2 | attr_accessor :x, :y 3 | 4 | def initialize(object_pool, x, y) 5 | super(object_pool) 6 | @x, @y = x, y 7 | ExplosionGraphics.new(self) 8 | ExplosionSounds.play 9 | inflict_damage 10 | end 11 | 12 | private 13 | 14 | def inflict_damage 15 | object_pool.nearby(self, 100).each do |obj| 16 | if obj.class == Tank 17 | obj.health.inflict_damage( 18 | Math.sqrt(3 * 100 - Utils.distance_between( 19 | obj.x, obj.y, x, y))) 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /08-ai/entities/game_object.rb: -------------------------------------------------------------------------------- 1 | class GameObject 2 | def initialize(object_pool) 3 | @components = [] 4 | @object_pool = object_pool 5 | @object_pool.objects << self 6 | end 7 | 8 | def components 9 | @components 10 | end 11 | 12 | def update 13 | @components.map(&:update) 14 | end 15 | 16 | def draw(viewport) 17 | @components.each { |c| c.draw(viewport) } 18 | end 19 | 20 | def removable? 21 | @removable 22 | end 23 | 24 | def mark_for_removal 25 | @removable = true 26 | end 27 | 28 | def box 29 | end 30 | 31 | def collide 32 | end 33 | 34 | protected 35 | 36 | def object_pool 37 | @object_pool 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /08-ai/entities/object_pool.rb: -------------------------------------------------------------------------------- 1 | class ObjectPool 2 | attr_accessor :objects, :map 3 | def initialize(map) 4 | @map = map 5 | @objects = [] 6 | end 7 | 8 | def nearby(object, max_distance) 9 | @objects.select do |obj| 10 | obj != object && 11 | (obj.x - object.x).abs < max_distance && 12 | (obj.y - object.y).abs < max_distance && 13 | Utils.distance_between( 14 | obj.x, obj.y, object.x, object.y) < max_distance 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /08-ai/entities/tank.rb: -------------------------------------------------------------------------------- 1 | class Tank < GameObject 2 | SHOOT_DELAY = 500 3 | attr_accessor :x, :y, :throttle_down, :direction, 4 | :gun_angle, :sounds, :physics, :graphics, :health, :input 5 | 6 | def initialize(object_pool, input) 7 | super(object_pool) 8 | @input = input 9 | @input.control(self) 10 | @physics = TankPhysics.new(self, object_pool) 11 | @sounds = TankSounds.new(self) 12 | @health = TankHealth.new(self, object_pool) 13 | @graphics = TankGraphics.new(self) 14 | @direction = rand(0..7) * 45 15 | @gun_angle = rand(0..360) 16 | end 17 | 18 | def box 19 | @physics.box 20 | end 21 | 22 | def shoot(target_x, target_y) 23 | if can_shoot? 24 | @last_shot = Gosu.milliseconds 25 | Bullet.new(object_pool, @x, @y, target_x, target_y).fire(self, 100) 26 | end 27 | end 28 | 29 | def can_shoot? 30 | Gosu.milliseconds - (@last_shot || 0) > SHOOT_DELAY 31 | end 32 | 33 | def to_s 34 | "Tank [#{@health.health}@#{@x}:#{@y}@#{@physics.speed.round(2)}px/tick]" 35 | end 36 | 37 | end 38 | -------------------------------------------------------------------------------- /08-ai/game_states/game_state.rb: -------------------------------------------------------------------------------- 1 | class GameState 2 | 3 | def self.switch(new_state) 4 | $window.state && $window.state.leave 5 | $window.state = new_state 6 | new_state.enter 7 | end 8 | 9 | def enter 10 | end 11 | 12 | def leave 13 | end 14 | 15 | def draw 16 | end 17 | 18 | def update 19 | end 20 | 21 | def needs_redraw? 22 | true 23 | end 24 | 25 | def button_down(id) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /08-ai/game_states/menu_state.rb: -------------------------------------------------------------------------------- 1 | require 'singleton' 2 | class MenuState < GameState 3 | include Singleton 4 | attr_accessor :play_state 5 | 6 | def initialize 7 | @message = Gosu::Image.from_text( 8 | $window, "Tanks Prototype", 9 | Gosu.default_font_name, 100) 10 | end 11 | 12 | def enter 13 | music.play(true) 14 | music.volume = 1 15 | end 16 | 17 | def leave 18 | music.volume = 0 19 | music.stop 20 | end 21 | 22 | def music 23 | @@music ||= Gosu::Song.new( 24 | $window, Utils.media_path('menu_music.mp3')) 25 | end 26 | 27 | def update 28 | continue_text = @play_state ? "C = Continue, " : "" 29 | @info = Gosu::Image.from_text( 30 | $window, "Q = Quit, #{continue_text}N = New Game", 31 | Gosu.default_font_name, 30) 32 | end 33 | 34 | def draw 35 | @message.draw( 36 | $window.width / 2 - @message.width / 2, 37 | $window.height / 2 - @message.height / 2, 38 | 10) 39 | @info.draw( 40 | $window.width / 2 - @info.width / 2, 41 | $window.height / 2 - @info.height / 2 + 200, 42 | 10) 43 | end 44 | 45 | def button_down(id) 46 | $window.close if id == Gosu::KbQ 47 | if id == Gosu::KbC && @play_state 48 | GameState.switch(@play_state) 49 | end 50 | if id == Gosu::KbN 51 | @play_state = PlayState.new 52 | GameState.switch(@play_state) 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /08-ai/game_window.rb: -------------------------------------------------------------------------------- 1 | class GameWindow < Gosu::Window 2 | attr_accessor :state 3 | 4 | def initialize 5 | super((ENV['w'] || 800).to_i, 6 | (ENV['h'] || 600).to_i, 7 | false) 8 | end 9 | 10 | def update 11 | Utils.track_update_interval 12 | @state.update 13 | end 14 | 15 | def draw 16 | @state.draw 17 | end 18 | 19 | def needs_redraw? 20 | @state.needs_redraw? 21 | end 22 | 23 | def needs_cursor? 24 | Utils.update_interval > 200 25 | end 26 | 27 | def button_down(id) 28 | @state.button_down(id) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /08-ai/main.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'gosu' 4 | 5 | root_dir = File.dirname(__FILE__) 6 | require_pattern = File.join(root_dir, '**/*.rb') 7 | @failed = [] 8 | 9 | # Dynamically require everything 10 | Dir.glob(require_pattern).each do |f| 11 | next if f.end_with?('/main.rb') 12 | begin 13 | require_relative f.gsub("#{root_dir}/", '') 14 | rescue 15 | # May fail if parent class not required yet 16 | @failed << f 17 | end 18 | end 19 | 20 | # Retry unresolved requires 21 | @failed.each do |f| 22 | require_relative f.gsub("#{root_dir}/", '') 23 | end 24 | 25 | $debug = false 26 | $window = GameWindow.new 27 | GameState.switch(MenuState.instance) 28 | $window.show 29 | -------------------------------------------------------------------------------- /09-polishing/entities/box.rb: -------------------------------------------------------------------------------- 1 | class Box < GameObject 2 | attr_reader :x, :y, :health, :graphics, :angle 3 | 4 | def initialize(object_pool, x, y) 5 | super(object_pool) 6 | @x, @y = x, y 7 | @graphics = BoxGraphics.new(self) 8 | @health = Health.new(self, object_pool, 10, true) 9 | @angle = rand(-15..15) 10 | end 11 | 12 | def on_collision(object) 13 | return unless object.physics.speed > 1.0 14 | @x, @y = Utils.point_at_distance(@x, @y, object.direction, 2) 15 | @box = nil 16 | end 17 | 18 | def box 19 | return @box if @box 20 | w = @graphics.width / 2 21 | h = @graphics.height / 2 22 | # Bounding box adjusted to trim shadows 23 | @box = [x - w + 4, y - h + 8, 24 | x + w , y - h + 8, 25 | x + w , y + h, 26 | x - w + 4, y + h] 27 | @box = Utils.rotate(@angle, @x, @y, *@box) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /09-polishing/entities/bullet.rb: -------------------------------------------------------------------------------- 1 | class Bullet < GameObject 2 | attr_accessor :x, :y, :target_x, :target_y, :source, :speed, :fired_at 3 | 4 | def initialize(object_pool, source_x, source_y, target_x, target_y) 5 | super(object_pool) 6 | @x, @y = source_x, source_y 7 | @target_x, @target_y = target_x, target_y 8 | BulletPhysics.new(self, object_pool) 9 | BulletGraphics.new(self) 10 | BulletSounds.play(self, object_pool.camera) 11 | end 12 | 13 | def box 14 | [x, y] 15 | end 16 | 17 | def explode 18 | Explosion.new(object_pool, @x, @y) 19 | mark_for_removal 20 | end 21 | 22 | def fire(source, speed) 23 | @source = source 24 | @speed = speed 25 | @fired_at = Gosu.milliseconds 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /09-polishing/entities/components/ai/tank_chasing_state.rb: -------------------------------------------------------------------------------- 1 | class TankChasingState < TankMotionState 2 | def initialize(object, vision, gun) 3 | super(object, vision) 4 | @object = object 5 | @vision = vision 6 | @gun = gun 7 | end 8 | 9 | def update 10 | change_direction if should_change_direction? 11 | drive 12 | end 13 | 14 | def change_direction 15 | @object.physics.change_direction( 16 | @gun.desired_gun_angle - 17 | @gun.desired_gun_angle % 45) 18 | 19 | @changed_direction_at = Gosu.milliseconds 20 | @will_keep_direction_for = turn_time 21 | end 22 | 23 | def drive_time 24 | 10000 25 | end 26 | 27 | def turn_time 28 | rand(300..600) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /09-polishing/entities/components/ai/tank_fighting_state.rb: -------------------------------------------------------------------------------- 1 | class TankFightingState < TankMotionState 2 | def initialize(object, vision) 3 | super 4 | @object = object 5 | @vision = vision 6 | end 7 | 8 | def update 9 | change_direction if should_change_direction? 10 | if substate_expired? 11 | rand > 0.2 ? drive : wait 12 | end 13 | end 14 | 15 | def change_direction 16 | change = case rand(0..100) 17 | when 0..20 18 | -45 19 | when 20..40 20 | 45 21 | when 40..60 22 | 90 23 | when 60..80 24 | -90 25 | when 80..90 26 | 135 27 | when 90..100 28 | -135 29 | end 30 | @object.physics.change_direction( 31 | @object.direction + change) 32 | @changed_direction_at = Gosu.milliseconds 33 | @will_keep_direction_for = turn_time 34 | end 35 | 36 | def wait_time 37 | rand(300..1000) 38 | end 39 | 40 | def drive_time 41 | rand(2000..5000) 42 | end 43 | 44 | def turn_time 45 | rand(500..2500) 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /09-polishing/entities/components/ai/tank_fleeing_state.rb: -------------------------------------------------------------------------------- 1 | class TankFleeingState < TankMotionState 2 | MAX_FLEE_TIME = 15 * 1000 # 15 seconds 3 | def initialize(object, vision, gun) 4 | super(object, vision) 5 | @object = object 6 | @vision = vision 7 | @gun = gun 8 | end 9 | 10 | def can_flee? 11 | return true unless @started_fleeing 12 | Gosu.milliseconds - @started_fleeing < MAX_FLEE_TIME 13 | end 14 | 15 | def enter 16 | @started_fleeing ||= Gosu.milliseconds 17 | end 18 | 19 | def update 20 | change_direction if should_change_direction? 21 | drive 22 | end 23 | 24 | def change_direction 25 | @object.physics.change_direction( 26 | 180 + @gun.desired_gun_angle - 27 | @gun.desired_gun_angle % 45) 28 | 29 | @changed_direction_at = Gosu.milliseconds 30 | @will_keep_direction_for = turn_time 31 | end 32 | 33 | def drive_time 34 | 10000 35 | end 36 | 37 | def turn_time 38 | rand(300..600) 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /09-polishing/entities/components/ai/tank_roaming_state.rb: -------------------------------------------------------------------------------- 1 | class TankRoamingState < TankMotionState 2 | def initialize(object, vision) 3 | super 4 | @object = object 5 | @vision = vision 6 | end 7 | 8 | def update 9 | change_direction if should_change_direction? 10 | if substate_expired? 11 | rand > 0.3 ? drive : wait 12 | end 13 | end 14 | 15 | def change_direction 16 | change = case rand(0..100) 17 | when 0..30 18 | -45 19 | when 30..60 20 | 45 21 | when 60..70 22 | 90 23 | when 80..90 24 | -90 25 | else 26 | 0 27 | end 28 | if change != 0 29 | @object.physics.change_direction( 30 | @object.direction + change) 31 | end 32 | @changed_direction_at = Gosu.milliseconds 33 | @will_keep_direction_for = turn_time 34 | end 35 | 36 | def wait_time 37 | rand(500..2000) 38 | end 39 | 40 | def drive_time 41 | rand(1000..5000) 42 | end 43 | 44 | def turn_time 45 | rand(2000..5000) 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /09-polishing/entities/components/ai/vision.rb: -------------------------------------------------------------------------------- 1 | class AiVision 2 | CACHE_TIMEOUT = 500 3 | attr_reader :in_sight 4 | 5 | def initialize(viewer, object_pool, distance) 6 | @viewer = viewer 7 | @object_pool = object_pool 8 | @distance = distance 9 | end 10 | 11 | def update 12 | @in_sight = @object_pool.nearby(@viewer, @distance) 13 | end 14 | 15 | def closest_tank 16 | now = Gosu.milliseconds 17 | @closest_tank = nil 18 | if now - (@cache_updated_at ||= 0) > CACHE_TIMEOUT 19 | @closest_tank = nil 20 | @cache_updated_at = now 21 | end 22 | @closest_tank ||= find_closest_tank 23 | end 24 | 25 | private 26 | 27 | def find_closest_tank 28 | @in_sight.select do |o| 29 | o.class == Tank && !o.health.dead? 30 | end.sort do |a, b| 31 | x, y = @viewer.x, @viewer.y 32 | d1 = Utils.distance_between(x, y, a.x, a.y) 33 | d2 = Utils.distance_between(x, y, b.x, b.y) 34 | d1 <=> d2 35 | end.first 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /09-polishing/entities/components/box_graphics.rb: -------------------------------------------------------------------------------- 1 | class BoxGraphics < Component 2 | def initialize(object) 3 | super(object) 4 | load_sprite 5 | end 6 | 7 | def draw(viewport) 8 | @box.draw_rot(x, y, 0, object.angle) 9 | Utils.mark_corners(object.box) if $debug 10 | end 11 | 12 | def height 13 | @box.height 14 | end 15 | 16 | def width 17 | @box.width 18 | end 19 | 20 | private 21 | 22 | def load_sprite 23 | frame = boxes.frame_list.sample 24 | @box = boxes.frame(frame) 25 | end 26 | 27 | def center_x 28 | @center_x ||= x - width / 2 29 | end 30 | 31 | def center_y 32 | @center_y ||= y - height / 2 33 | end 34 | 35 | def boxes 36 | @@boxes ||= Gosu::TexturePacker.load_json($window, 37 | Utils.media_path('boxes_barrels.json')) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /09-polishing/entities/components/bullet_graphics.rb: -------------------------------------------------------------------------------- 1 | class BulletGraphics < Component 2 | def draw(viewport) 3 | image.draw(x - 8, y - 8, 1) 4 | Utils.mark_corners(object.box) if $debug 5 | end 6 | 7 | private 8 | 9 | def image 10 | @@bullet ||= Gosu::Image.new( 11 | $window, Utils.media_path('bullet.png'), false) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /09-polishing/entities/components/bullet_sounds.rb: -------------------------------------------------------------------------------- 1 | class BulletSounds 2 | class << self 3 | def play(object, camera) 4 | volume, pan = Utils.volume_and_pan(object, camera) 5 | sound.play(object.object_id, pan, volume) 6 | end 7 | 8 | private 9 | 10 | def sound 11 | @@sound ||= StereoSample.new( 12 | $window, Utils.media_path('fire.mp3')) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /09-polishing/entities/components/component.rb: -------------------------------------------------------------------------------- 1 | class Component 2 | attr_reader :object # better performance 3 | 4 | def initialize(game_object = nil) 5 | self.object = game_object 6 | end 7 | 8 | def update 9 | # override 10 | end 11 | 12 | def draw(viewport) 13 | # override 14 | end 15 | 16 | protected 17 | 18 | def object=(obj) 19 | if obj 20 | @object = obj 21 | obj.components << self 22 | end 23 | end 24 | 25 | def x 26 | @object.x 27 | end 28 | 29 | def y 30 | @object.y 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /09-polishing/entities/components/damage_graphics.rb: -------------------------------------------------------------------------------- 1 | class DamageGraphics < Component 2 | def initialize(object_pool) 3 | super 4 | @image = images.sample 5 | @angle = rand(0..360) 6 | end 7 | 8 | def draw(viewport) 9 | @image.draw_rot(x, y, 0, @angle) 10 | end 11 | 12 | private 13 | 14 | def images 15 | @@images ||= (1..4).map do |i| 16 | Gosu::Image.new($window, 17 | Utils.media_path("damage#{i}.png"), false) 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /09-polishing/entities/components/explosion_graphics.rb: -------------------------------------------------------------------------------- 1 | class ExplosionGraphics < Component 2 | FRAME_DELAY = 16.66 # ms 3 | 4 | def initialize(game_object) 5 | super 6 | @current_frame = 0 7 | end 8 | 9 | def draw(viewport) 10 | image = current_frame 11 | image.draw( 12 | x - image.width / 2 + 3, 13 | y - image.height / 2 - 35, 14 | 20) 15 | end 16 | 17 | def update 18 | now = Gosu.milliseconds 19 | delta = now - (@last_frame ||= now) 20 | if delta > FRAME_DELAY 21 | @last_frame = now 22 | end 23 | @current_frame += (delta / FRAME_DELAY).floor 24 | object.mark_for_removal if done? 25 | end 26 | 27 | private 28 | 29 | def current_frame 30 | animation[@current_frame % animation.size] 31 | end 32 | 33 | def done? 34 | @done ||= @current_frame >= animation.size 35 | end 36 | 37 | def animation 38 | @@animation ||= 39 | Gosu::Image.load_tiles( 40 | $window, Utils.media_path('explosion.png'), 41 | 128, 128, false) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /09-polishing/entities/components/explosion_sounds.rb: -------------------------------------------------------------------------------- 1 | class ExplosionSounds 2 | class << self 3 | def play(object, camera) 4 | volume, pan = Utils.volume_and_pan(object, camera) 5 | sound.play(object.object_id, pan, volume) 6 | end 7 | 8 | private 9 | 10 | def sound 11 | @@sound ||= StereoSample.new( 12 | $window, Utils.media_path('explosion.mp3')) 13 | end 14 | end 15 | end 16 | 17 | -------------------------------------------------------------------------------- /09-polishing/entities/components/player_sounds.rb: -------------------------------------------------------------------------------- 1 | class PlayerSounds 2 | class << self 3 | def respawn(object, camera) 4 | volume, pan = Utils.volume_and_pan(object, camera) 5 | respawn_sound.play(object.object_id, pan, volume * 0.5) 6 | end 7 | 8 | private 9 | 10 | def respawn_sound 11 | @@respawn ||= StereoSample.new( 12 | $window, Utils.media_path('respawn.wav')) 13 | end 14 | end 15 | end 16 | 17 | -------------------------------------------------------------------------------- /09-polishing/entities/components/tank_graphics.rb: -------------------------------------------------------------------------------- 1 | class TankGraphics < Component 2 | def initialize(game_object) 3 | super(game_object) 4 | @body_normal = units.frame('tank1_body.png') 5 | @shadow_normal = units.frame('tank1_body_shadow.png') 6 | @gun_normal = units.frame('tank1_dualgun.png') 7 | @body_dead = units.frame('tank1_body_destroyed.png') 8 | @shadow_dead = units.frame('tank1_body_destroyed_shadow.png') 9 | @gun_dead = nil 10 | update 11 | end 12 | 13 | def update 14 | if object && object.health.dead? 15 | @body = @body_dead 16 | @gun = @gun_dead 17 | @shadow = @shadow_dead 18 | else 19 | @body = @body_normal 20 | @gun = @gun_normal 21 | @shadow = @shadow_normal 22 | end 23 | end 24 | 25 | def draw(viewport) 26 | @shadow.draw_rot(x - 1, y - 1, 0, object.direction) 27 | @body.draw_rot(x, y, 1, object.direction) 28 | @gun.draw_rot(x, y, 2, object.gun_angle) if @gun 29 | Utils.mark_corners(object.box) if $debug 30 | end 31 | 32 | def width 33 | @body.width 34 | end 35 | 36 | def height 37 | @body.height 38 | end 39 | 40 | private 41 | 42 | def units 43 | @@units = Gosu::TexturePacker.load_json( 44 | $window, Utils.media_path('ground_units.json'), :precise) 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /09-polishing/entities/components/tank_health.rb: -------------------------------------------------------------------------------- 1 | class TankHealth < Health 2 | RESPAWN_DELAY = 5000 3 | attr_accessor :health 4 | 5 | def initialize(object, object_pool) 6 | super(object, object_pool, 100, true) 7 | end 8 | 9 | def should_respawn? 10 | Gosu.milliseconds - @death_time > RESPAWN_DELAY 11 | end 12 | 13 | protected 14 | 15 | def draw? 16 | true 17 | end 18 | 19 | def after_death 20 | @death_time = Gosu.milliseconds 21 | if Thread.list.count < 8 22 | Thread.new do 23 | sleep(rand(0.1..0.3)) 24 | Explosion.new(@object_pool, x, y) 25 | end 26 | else 27 | Explosion.new(@object_pool, x, y) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /09-polishing/entities/components/tank_sounds.rb: -------------------------------------------------------------------------------- 1 | class TankSounds < Component 2 | def initialize(object, object_pool) 3 | super(object) 4 | @object_pool = object_pool 5 | end 6 | 7 | def update 8 | id = object.object_id 9 | if object.physics.moving? 10 | move_volume = Utils.volume( 11 | object, @object_pool.camera) 12 | pan = Utils.pan(object, @object_pool.camera) 13 | if driving_sound.paused?(id) 14 | driving_sound.resume(id) 15 | elsif driving_sound.stopped?(id) 16 | driving_sound.play(id, pan, 0.5, 1, true) 17 | end 18 | driving_sound.volume_and_pan(id, move_volume * 0.5, pan) 19 | else 20 | if driving_sound.playing?(id) 21 | driving_sound.pause(id) 22 | end 23 | end 24 | end 25 | 26 | def collide 27 | vol, pan = Utils.volume_and_pan( 28 | object, @object_pool.camera) 29 | crash_sound.play(self.object_id, pan, vol, 1, false) 30 | end 31 | 32 | private 33 | 34 | def driving_sound 35 | @@driving_sound ||= StereoSample.new( 36 | $window, Utils.media_path('tank_driving.mp3')) 37 | end 38 | 39 | def crash_sound 40 | @@crash_sound ||= StereoSample.new( 41 | $window, Utils.media_path('metal_interaction2.wav')) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /09-polishing/entities/damage.rb: -------------------------------------------------------------------------------- 1 | class Damage < GameObject 2 | MAX_INSTANCES = 100 3 | attr_accessor :x, :y 4 | @@instances = [] 5 | 6 | def initialize(object_pool, x, y) 7 | super(object_pool) 8 | DamageGraphics.new(self) 9 | @x, @y = x, y 10 | track(self) 11 | end 12 | 13 | def effect? 14 | true 15 | end 16 | 17 | private 18 | 19 | def track(instance) 20 | if @@instances.size < MAX_INSTANCES 21 | @@instances << instance 22 | else 23 | out = @@instances.shift 24 | out.mark_for_removal 25 | @@instances << instance 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /09-polishing/entities/explosion.rb: -------------------------------------------------------------------------------- 1 | class Explosion < GameObject 2 | attr_accessor :x, :y 3 | 4 | def initialize(object_pool, x, y) 5 | super(object_pool) 6 | @object_pool = object_pool 7 | @x, @y = x, y 8 | if @object_pool.map.can_move_to?(x, y) 9 | Damage.new(@object_pool, @x, @y) 10 | end 11 | ExplosionGraphics.new(self) 12 | ExplosionSounds.play(self, object_pool.camera) 13 | inflict_damage 14 | end 15 | 16 | def effect? 17 | true 18 | end 19 | 20 | def mark_for_removal 21 | super 22 | end 23 | 24 | private 25 | 26 | def inflict_damage 27 | object_pool.nearby(self, 100).each do |obj| 28 | if obj.respond_to?(:health) 29 | obj.health.inflict_damage( 30 | Math.sqrt(3 * 100 - Utils.distance_between( 31 | obj.x, obj.y, x, y))) 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /09-polishing/entities/game_object.rb: -------------------------------------------------------------------------------- 1 | class GameObject 2 | def initialize(object_pool) 3 | @components = [] 4 | @object_pool = object_pool 5 | @object_pool.objects << self 6 | end 7 | 8 | def components 9 | @components 10 | end 11 | 12 | def update 13 | @components.map(&:update) 14 | end 15 | 16 | def draw(viewport) 17 | @components.each { |c| c.draw(viewport) } 18 | end 19 | 20 | def removable? 21 | @removable 22 | end 23 | 24 | def mark_for_removal 25 | @removable = true 26 | end 27 | 28 | def on_collision(object) 29 | end 30 | 31 | def effect? 32 | false 33 | end 34 | 35 | def box 36 | end 37 | 38 | def collide 39 | end 40 | 41 | protected 42 | 43 | def object_pool 44 | @object_pool 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /09-polishing/entities/object_pool.rb: -------------------------------------------------------------------------------- 1 | class ObjectPool 2 | attr_accessor :objects, :map, :camera 3 | 4 | def initialize 5 | @objects = [] 6 | end 7 | 8 | def nearby(object, max_distance) 9 | non_effects.select do |obj| 10 | obj != object && 11 | (obj.x - object.x).abs < max_distance && 12 | (obj.y - object.y).abs < max_distance && 13 | Utils.distance_between( 14 | obj.x, obj.y, object.x, object.y) < max_distance 15 | end 16 | end 17 | 18 | def non_effects 19 | @objects.reject(&:effect?) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /09-polishing/entities/tree.rb: -------------------------------------------------------------------------------- 1 | class Tree < GameObject 2 | attr_reader :x, :y, :health, :graphics 3 | 4 | def initialize(object_pool, x, y, seed) 5 | super(object_pool) 6 | @x, @y = x, y 7 | @graphics = TreeGraphics.new(self, seed) 8 | @health = Health.new(self, object_pool, 30, false) 9 | @angle = rand(-15..15) 10 | end 11 | 12 | def on_collision(object) 13 | @graphics.shake(object.direction) 14 | end 15 | 16 | def box 17 | [x, y] 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /09-polishing/game_states/game_state.rb: -------------------------------------------------------------------------------- 1 | class GameState 2 | 3 | def self.switch(new_state) 4 | $window.state && $window.state.leave 5 | $window.state = new_state 6 | new_state.enter 7 | end 8 | 9 | def enter 10 | end 11 | 12 | def leave 13 | end 14 | 15 | def draw 16 | end 17 | 18 | def update 19 | end 20 | 21 | def needs_redraw? 22 | true 23 | end 24 | 25 | def button_down(id) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /09-polishing/game_window.rb: -------------------------------------------------------------------------------- 1 | class GameWindow < Gosu::Window 2 | attr_accessor :state 3 | 4 | def initialize 5 | super((ENV['w'] || 800).to_i, 6 | (ENV['h'] || 600).to_i, 7 | (ENV['fs'] ? true : false)) 8 | end 9 | 10 | def update 11 | Utils.track_update_interval 12 | @state.update 13 | end 14 | 15 | def draw 16 | @state.draw 17 | end 18 | 19 | def needs_redraw? 20 | @state.needs_redraw? 21 | end 22 | 23 | def needs_cursor? 24 | Utils.update_interval > 200 25 | end 26 | 27 | def button_down(id) 28 | @state.button_down(id) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /09-polishing/main.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'gosu' 4 | require 'gosu_texture_packer' 5 | require 'perlin_noise' 6 | 7 | root_dir = File.dirname(__FILE__) 8 | require_pattern = File.join(root_dir, '**/*.rb') 9 | @failed = [] 10 | 11 | # Dynamically require everything 12 | Dir.glob(require_pattern).each do |f| 13 | next if f.end_with?('/main.rb') 14 | begin 15 | require_relative f.gsub("#{root_dir}/", '') 16 | rescue 17 | # May fail if parent class not required yet 18 | @failed << f 19 | end 20 | end 21 | 22 | # Retry unresolved requires 23 | @failed.each do |f| 24 | require_relative f.gsub("#{root_dir}/", '') 25 | end 26 | 27 | $debug = false 28 | $window = GameWindow.new 29 | GameState.switch(MenuState.instance) 30 | $window.show 31 | -------------------------------------------------------------------------------- /09-polishing/misc/names.rb: -------------------------------------------------------------------------------- 1 | class Names 2 | def initialize(file) 3 | @names = File.read(file).split("\n").reject do |n| 4 | n.size > 12 5 | end 6 | end 7 | 8 | def random 9 | name = @names.sample 10 | @names.delete(name) 11 | name 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /10-partitioning/entities/box.rb: -------------------------------------------------------------------------------- 1 | class Box < GameObject 2 | attr_reader :health, :graphics, :angle 3 | 4 | def initialize(object_pool, x, y) 5 | super 6 | @graphics = BoxGraphics.new(self) 7 | @health = Health.new(self, object_pool, 10, true) 8 | @angle = rand(-15..15) 9 | end 10 | 11 | def on_collision(object) 12 | return unless object.physics.speed > 1.0 13 | move(*Utils.point_at_distance(@x, @y, object.direction, 2)) 14 | @box = nil 15 | end 16 | 17 | def box 18 | return @box if @box 19 | w = @graphics.width / 2 20 | h = @graphics.height / 2 21 | # Bounding box adjusted to trim shadows 22 | @box = [x - w + 4, y - h + 8, 23 | x + w , y - h + 8, 24 | x + w , y + h, 25 | x - w + 4, y + h] 26 | @box = Utils.rotate(@angle, @x, @y, *@box) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /10-partitioning/entities/bullet.rb: -------------------------------------------------------------------------------- 1 | class Bullet < GameObject 2 | attr_accessor :target_x, :target_y, :source, :speed, :fired_at 3 | 4 | def initialize(object_pool, source_x, source_y, target_x, target_y) 5 | super(object_pool, source_x, source_y) 6 | @target_x, @target_y = target_x, target_y 7 | BulletPhysics.new(self, object_pool) 8 | BulletGraphics.new(self) 9 | BulletSounds.play(self, object_pool.camera) 10 | end 11 | 12 | def box 13 | [@x, @y] 14 | end 15 | 16 | def explode 17 | Explosion.new(object_pool, @x, @y) 18 | mark_for_removal 19 | end 20 | 21 | def fire(source, speed) 22 | @source = source 23 | @speed = speed 24 | @fired_at = Gosu.milliseconds 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /10-partitioning/entities/components/ai/tank_chasing_state.rb: -------------------------------------------------------------------------------- 1 | class TankChasingState < TankMotionState 2 | def initialize(object, vision, gun) 3 | super(object, vision) 4 | @object = object 5 | @vision = vision 6 | @gun = gun 7 | end 8 | 9 | def update 10 | change_direction if should_change_direction? 11 | drive 12 | end 13 | 14 | def change_direction 15 | @object.physics.change_direction( 16 | @gun.desired_gun_angle - 17 | @gun.desired_gun_angle % 45) 18 | 19 | @changed_direction_at = Gosu.milliseconds 20 | @will_keep_direction_for = turn_time 21 | end 22 | 23 | def drive_time 24 | 10000 25 | end 26 | 27 | def turn_time 28 | rand(300..600) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /10-partitioning/entities/components/ai/tank_fighting_state.rb: -------------------------------------------------------------------------------- 1 | class TankFightingState < TankMotionState 2 | def initialize(object, vision) 3 | super 4 | @object = object 5 | @vision = vision 6 | end 7 | 8 | def update 9 | change_direction if should_change_direction? 10 | if substate_expired? 11 | rand > 0.2 ? drive : wait 12 | end 13 | end 14 | 15 | def change_direction 16 | change = case rand(0..100) 17 | when 0..20 18 | -45 19 | when 20..40 20 | 45 21 | when 40..60 22 | 90 23 | when 60..80 24 | -90 25 | when 80..90 26 | 135 27 | when 90..100 28 | -135 29 | end 30 | @object.physics.change_direction( 31 | @object.direction + change) 32 | @changed_direction_at = Gosu.milliseconds 33 | @will_keep_direction_for = turn_time 34 | end 35 | 36 | def wait_time 37 | rand(300..1000) 38 | end 39 | 40 | def drive_time 41 | rand(2000..5000) 42 | end 43 | 44 | def turn_time 45 | rand(500..2500) 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /10-partitioning/entities/components/ai/tank_fleeing_state.rb: -------------------------------------------------------------------------------- 1 | class TankFleeingState < TankMotionState 2 | MAX_FLEE_TIME = 15 * 1000 # 15 seconds 3 | def initialize(object, vision, gun) 4 | super(object, vision) 5 | @object = object 6 | @vision = vision 7 | @gun = gun 8 | end 9 | 10 | def can_flee? 11 | return true unless @started_fleeing 12 | Gosu.milliseconds - @started_fleeing < MAX_FLEE_TIME 13 | end 14 | 15 | def enter 16 | @started_fleeing ||= Gosu.milliseconds 17 | end 18 | 19 | def update 20 | change_direction if should_change_direction? 21 | drive 22 | end 23 | 24 | def change_direction 25 | @object.physics.change_direction( 26 | 180 + @gun.desired_gun_angle - 27 | @gun.desired_gun_angle % 45) 28 | 29 | @changed_direction_at = Gosu.milliseconds 30 | @will_keep_direction_for = turn_time 31 | end 32 | 33 | def drive_time 34 | 10000 35 | end 36 | 37 | def turn_time 38 | rand(300..600) 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /10-partitioning/entities/components/ai/tank_roaming_state.rb: -------------------------------------------------------------------------------- 1 | class TankRoamingState < TankMotionState 2 | def initialize(object, vision) 3 | super 4 | @object = object 5 | @vision = vision 6 | end 7 | 8 | def update 9 | change_direction if should_change_direction? 10 | if substate_expired? 11 | rand > 0.3 ? drive : wait 12 | end 13 | end 14 | 15 | def change_direction 16 | change = case rand(0..100) 17 | when 0..30 18 | -45 19 | when 30..60 20 | 45 21 | when 60..70 22 | 90 23 | when 80..90 24 | -90 25 | else 26 | 0 27 | end 28 | if change != 0 29 | @object.physics.change_direction( 30 | @object.direction + change) 31 | end 32 | @changed_direction_at = Gosu.milliseconds 33 | @will_keep_direction_for = turn_time 34 | end 35 | 36 | def wait_time 37 | rand(500..2000) 38 | end 39 | 40 | def drive_time 41 | rand(1000..5000) 42 | end 43 | 44 | def turn_time 45 | rand(2000..5000) 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /10-partitioning/entities/components/ai/vision.rb: -------------------------------------------------------------------------------- 1 | class AiVision 2 | CACHE_TIMEOUT = 500 3 | attr_reader :in_sight 4 | 5 | def initialize(viewer, object_pool, distance) 6 | @viewer = viewer 7 | @object_pool = object_pool 8 | @distance = distance 9 | end 10 | 11 | def update 12 | @in_sight = @object_pool.nearby(@viewer, @distance) 13 | end 14 | 15 | def closest_tank 16 | now = Gosu.milliseconds 17 | @closest_tank = nil 18 | if now - (@cache_updated_at ||= 0) > CACHE_TIMEOUT 19 | @closest_tank = nil 20 | @cache_updated_at = now 21 | end 22 | @closest_tank ||= find_closest_tank 23 | end 24 | 25 | private 26 | 27 | def find_closest_tank 28 | @in_sight.select do |o| 29 | o.class == Tank && !o.health.dead? 30 | end.sort do |a, b| 31 | x, y = @viewer.x, @viewer.y 32 | d1 = Utils.distance_between(x, y, a.x, a.y) 33 | d2 = Utils.distance_between(x, y, b.x, b.y) 34 | d1 <=> d2 35 | end.first 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /10-partitioning/entities/components/box_graphics.rb: -------------------------------------------------------------------------------- 1 | class BoxGraphics < Component 2 | def initialize(object) 3 | super(object) 4 | load_sprite 5 | end 6 | 7 | def draw(viewport) 8 | @box.draw_rot(x, y, 0, object.angle) 9 | Utils.mark_corners(object.box) if $debug 10 | end 11 | 12 | def height 13 | @box.height 14 | end 15 | 16 | def width 17 | @box.width 18 | end 19 | 20 | private 21 | 22 | def load_sprite 23 | frame = boxes.frame_list.sample 24 | @box = boxes.frame(frame) 25 | end 26 | 27 | def center_x 28 | @center_x ||= x - width / 2 29 | end 30 | 31 | def center_y 32 | @center_y ||= y - height / 2 33 | end 34 | 35 | def boxes 36 | @@boxes ||= Gosu::TexturePacker.load_json($window, 37 | Utils.media_path('boxes_barrels.json')) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /10-partitioning/entities/components/bullet_graphics.rb: -------------------------------------------------------------------------------- 1 | class BulletGraphics < Component 2 | def draw(viewport) 3 | image.draw(x - 8, y - 8, 1) 4 | Utils.mark_corners(object.box) if $debug 5 | end 6 | 7 | private 8 | 9 | def image 10 | @@bullet ||= Gosu::Image.new( 11 | $window, Utils.media_path('bullet.png'), false) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /10-partitioning/entities/components/bullet_sounds.rb: -------------------------------------------------------------------------------- 1 | class BulletSounds 2 | class << self 3 | def play(object, camera) 4 | volume, pan = Utils.volume_and_pan(object, camera) 5 | sound.play(object.object_id, pan, volume) 6 | end 7 | 8 | private 9 | 10 | def sound 11 | @@sound ||= StereoSample.new( 12 | $window, Utils.media_path('fire.mp3')) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /10-partitioning/entities/components/component.rb: -------------------------------------------------------------------------------- 1 | class Component 2 | attr_reader :object # better performance 3 | 4 | def initialize(game_object = nil) 5 | self.object = game_object 6 | end 7 | 8 | def update 9 | # override 10 | end 11 | 12 | def draw(viewport) 13 | # override 14 | end 15 | 16 | protected 17 | 18 | def object=(obj) 19 | if obj 20 | @object = obj 21 | obj.components << self 22 | end 23 | end 24 | 25 | def x 26 | @object.x 27 | end 28 | 29 | def y 30 | @object.y 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /10-partitioning/entities/components/damage_graphics.rb: -------------------------------------------------------------------------------- 1 | class DamageGraphics < Component 2 | def initialize(object_pool) 3 | super 4 | @image = images.sample 5 | @angle = rand(0..360) 6 | end 7 | 8 | def draw(viewport) 9 | @image.draw_rot(x, y, 0, @angle) 10 | end 11 | 12 | private 13 | 14 | def images 15 | @@images ||= (1..4).map do |i| 16 | Gosu::Image.new($window, 17 | Utils.media_path("damage#{i}.png"), false) 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /10-partitioning/entities/components/explosion_graphics.rb: -------------------------------------------------------------------------------- 1 | class ExplosionGraphics < Component 2 | FRAME_DELAY = 16.66 # ms 3 | 4 | def initialize(game_object) 5 | super 6 | @current_frame = 0 7 | end 8 | 9 | def draw(viewport) 10 | image = current_frame 11 | image.draw( 12 | x - image.width / 2 + 3, 13 | y - image.height / 2 - 35, 14 | 20) 15 | end 16 | 17 | def update 18 | now = Gosu.milliseconds 19 | delta = now - (@last_frame ||= now) 20 | if delta > FRAME_DELAY 21 | @last_frame = now 22 | end 23 | @current_frame += (delta / FRAME_DELAY).floor 24 | object.mark_for_removal if done? 25 | end 26 | 27 | private 28 | 29 | def current_frame 30 | animation[@current_frame % animation.size] 31 | end 32 | 33 | def done? 34 | @done ||= @current_frame >= animation.size 35 | end 36 | 37 | def animation 38 | @@animation ||= 39 | Gosu::Image.load_tiles( 40 | $window, Utils.media_path('explosion.png'), 41 | 128, 128, false) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /10-partitioning/entities/components/explosion_sounds.rb: -------------------------------------------------------------------------------- 1 | class ExplosionSounds 2 | class << self 3 | def play(object, camera) 4 | volume, pan = Utils.volume_and_pan(object, camera) 5 | sound.play(object.object_id, pan, volume) 6 | end 7 | 8 | private 9 | 10 | def sound 11 | @@sound ||= StereoSample.new( 12 | $window, Utils.media_path('explosion.mp3')) 13 | end 14 | end 15 | end 16 | 17 | -------------------------------------------------------------------------------- /10-partitioning/entities/components/player_sounds.rb: -------------------------------------------------------------------------------- 1 | class PlayerSounds 2 | class << self 3 | def respawn(object, camera) 4 | volume, pan = Utils.volume_and_pan(object, camera) 5 | respawn_sound.play(object.object_id, pan, volume * 0.5) 6 | end 7 | 8 | private 9 | 10 | def respawn_sound 11 | @@respawn ||= StereoSample.new( 12 | $window, Utils.media_path('respawn.wav')) 13 | end 14 | end 15 | end 16 | 17 | -------------------------------------------------------------------------------- /10-partitioning/entities/components/tank_graphics.rb: -------------------------------------------------------------------------------- 1 | class TankGraphics < Component 2 | def initialize(game_object) 3 | super(game_object) 4 | @body_normal = units.frame('tank1_body.png') 5 | @shadow_normal = units.frame('tank1_body_shadow.png') 6 | @gun_normal = units.frame('tank1_dualgun.png') 7 | @body_dead = units.frame('tank1_body_destroyed.png') 8 | @shadow_dead = units.frame('tank1_body_destroyed_shadow.png') 9 | @gun_dead = nil 10 | update 11 | end 12 | 13 | def update 14 | if object && object.health.dead? 15 | @body = @body_dead 16 | @gun = @gun_dead 17 | @shadow = @shadow_dead 18 | else 19 | @body = @body_normal 20 | @gun = @gun_normal 21 | @shadow = @shadow_normal 22 | end 23 | end 24 | 25 | def draw(viewport) 26 | @shadow.draw_rot(x - 1, y - 1, 0, object.direction) 27 | @body.draw_rot(x, y, 1, object.direction) 28 | @gun.draw_rot(x, y, 2, object.gun_angle) if @gun 29 | Utils.mark_corners(object.box) if $debug 30 | end 31 | 32 | def width 33 | @body.width 34 | end 35 | 36 | def height 37 | @body.height 38 | end 39 | 40 | private 41 | 42 | def units 43 | @@units = Gosu::TexturePacker.load_json( 44 | $window, Utils.media_path('ground_units.json'), :precise) 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /10-partitioning/entities/components/tank_health.rb: -------------------------------------------------------------------------------- 1 | class TankHealth < Health 2 | RESPAWN_DELAY = 5000 3 | attr_accessor :health 4 | 5 | def initialize(object, object_pool) 6 | super(object, object_pool, 100, true) 7 | end 8 | 9 | def should_respawn? 10 | Gosu.milliseconds - @death_time > RESPAWN_DELAY 11 | end 12 | 13 | protected 14 | 15 | def draw? 16 | true 17 | end 18 | 19 | def after_death 20 | @death_time = Gosu.milliseconds 21 | Thread.new do 22 | sleep(rand(0.1..0.3)) 23 | Explosion.new(@object_pool, x, y) 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /10-partitioning/entities/components/tank_sounds.rb: -------------------------------------------------------------------------------- 1 | class TankSounds < Component 2 | def initialize(object, object_pool) 3 | super(object) 4 | @object_pool = object_pool 5 | end 6 | 7 | def update 8 | id = object.object_id 9 | if object.physics.moving? 10 | move_volume = Utils.volume( 11 | object, @object_pool.camera) 12 | pan = Utils.pan(object, @object_pool.camera) 13 | if driving_sound.paused?(id) 14 | driving_sound.resume(id) 15 | elsif driving_sound.stopped?(id) 16 | driving_sound.play(id, pan, 0.5, 1, true) 17 | end 18 | driving_sound.volume_and_pan(id, move_volume * 0.5, pan) 19 | else 20 | if driving_sound.playing?(id) 21 | driving_sound.pause(id) 22 | end 23 | end 24 | end 25 | 26 | def collide 27 | vol, pan = Utils.volume_and_pan( 28 | object, @object_pool.camera) 29 | crash_sound.play(self.object_id, pan, vol, 1, false) 30 | end 31 | 32 | private 33 | 34 | def driving_sound 35 | @@driving_sound ||= StereoSample.new( 36 | $window, Utils.media_path('tank_driving.mp3')) 37 | end 38 | 39 | def crash_sound 40 | @@crash_sound ||= StereoSample.new( 41 | $window, Utils.media_path('metal_interaction2.wav')) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /10-partitioning/entities/damage.rb: -------------------------------------------------------------------------------- 1 | class Damage < GameObject 2 | MAX_INSTANCES = 300 3 | @@instances = [] 4 | 5 | def initialize(object_pool, x, y) 6 | super 7 | DamageGraphics.new(self) 8 | track(self) 9 | end 10 | 11 | def effect? 12 | true 13 | end 14 | 15 | private 16 | 17 | def track(instance) 18 | if @@instances.size < MAX_INSTANCES 19 | @@instances << instance 20 | else 21 | out = @@instances.shift 22 | out.mark_for_removal 23 | @@instances << instance 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /10-partitioning/entities/explosion.rb: -------------------------------------------------------------------------------- 1 | class Explosion < GameObject 2 | 3 | def initialize(object_pool, x, y) 4 | super 5 | @object_pool = object_pool 6 | if @object_pool.map.can_move_to?(x, y) 7 | Damage.new(@object_pool, x, y) 8 | end 9 | ExplosionGraphics.new(self) 10 | ExplosionSounds.play(self, object_pool.camera) 11 | inflict_damage 12 | end 13 | 14 | def effect? 15 | true 16 | end 17 | 18 | def mark_for_removal 19 | super 20 | end 21 | 22 | private 23 | 24 | def inflict_damage 25 | object_pool.nearby(self, 100).each do |obj| 26 | if obj.respond_to?(:health) 27 | obj.health.inflict_damage( 28 | Math.sqrt(3 * 100 - Utils.distance_between( 29 | obj.x, obj.y, @x, @y))) 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /10-partitioning/entities/game_object.rb: -------------------------------------------------------------------------------- 1 | class GameObject 2 | attr_reader :x, :y, :location, :components 3 | def initialize(object_pool, x, y) 4 | @x, @y = x, y 5 | @location = [x, y] 6 | @components = [] 7 | @object_pool = object_pool 8 | @object_pool.add(self) 9 | end 10 | 11 | def move(new_x, new_y) 12 | return if new_x == @x && new_y == @y 13 | @object_pool.tree_remove(self) 14 | @x = new_x 15 | @y = new_y 16 | @location = [new_x, new_y] 17 | @object_pool.tree_insert(self) 18 | end 19 | 20 | def update 21 | @components.map(&:update) 22 | end 23 | 24 | def draw(viewport) 25 | @components.each { |c| c.draw(viewport) } 26 | end 27 | 28 | def removable? 29 | @removable 30 | end 31 | 32 | def mark_for_removal 33 | @removable = true 34 | end 35 | 36 | def on_collision(object) 37 | end 38 | 39 | def effect? 40 | false 41 | end 42 | 43 | def box 44 | end 45 | 46 | def collide 47 | end 48 | 49 | protected 50 | 51 | def object_pool 52 | @object_pool 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /10-partitioning/entities/object_pool.rb: -------------------------------------------------------------------------------- 1 | class ObjectPool 2 | attr_accessor :map, :camera, :objects 3 | 4 | def size 5 | @objects.size 6 | end 7 | 8 | def initialize(box) 9 | @tree = QuadTree.new(box) 10 | @objects = [] 11 | end 12 | 13 | def add(object) 14 | @objects << object 15 | @tree.insert(object) 16 | end 17 | 18 | def tree_remove(object) 19 | @tree.remove(object) 20 | end 21 | 22 | def tree_insert(object) 23 | @tree.insert(object) 24 | end 25 | 26 | def update_all 27 | @objects.map(&:update) 28 | @objects.reject! do |o| 29 | if o.removable? 30 | @tree.remove(o) 31 | true 32 | end 33 | end 34 | end 35 | 36 | def nearby(object, max_distance) 37 | cx, cy = object.location 38 | hx, hy = cx + max_distance, cy + max_distance 39 | # Fast, rough results 40 | results = @tree.query_range( 41 | AxisAlignedBoundingBox.new([cx, cy], [hx, hy])) 42 | # Sift through to select fine-grained results 43 | results.select do |o| 44 | o != object && 45 | Utils.distance_between( 46 | o.x, o.y, object.x, object.y) <= max_distance 47 | end 48 | end 49 | 50 | def query_range(box) 51 | @tree.query_range(box) 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /10-partitioning/entities/tree.rb: -------------------------------------------------------------------------------- 1 | class Tree < GameObject 2 | attr_reader :health, :graphics 3 | 4 | def initialize(object_pool, x, y, seed) 5 | super(object_pool, x, y) 6 | @graphics = TreeGraphics.new(self, seed) 7 | @health = Health.new(self, object_pool, 30, false) 8 | @angle = rand(-15..15) 9 | end 10 | 11 | def on_collision(object) 12 | @graphics.shake(object.direction) 13 | end 14 | 15 | def box 16 | [@x, @y] 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /10-partitioning/game_states/game_state.rb: -------------------------------------------------------------------------------- 1 | class GameState 2 | 3 | def self.switch(new_state) 4 | $window.state && $window.state.leave 5 | $window.state = new_state 6 | new_state.enter 7 | end 8 | 9 | def enter 10 | end 11 | 12 | def leave 13 | end 14 | 15 | def draw 16 | end 17 | 18 | def update 19 | end 20 | 21 | def needs_redraw? 22 | true 23 | end 24 | 25 | def button_down(id) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /10-partitioning/game_window.rb: -------------------------------------------------------------------------------- 1 | class GameWindow < Gosu::Window 2 | attr_accessor :state 3 | 4 | def initialize 5 | super((ENV['w'] || 800).to_i, 6 | (ENV['h'] || 600).to_i, 7 | (ENV['fs'] ? true : false)) 8 | end 9 | 10 | def update 11 | Utils.track_update_interval 12 | @state.update 13 | end 14 | 15 | def draw 16 | @state.draw 17 | end 18 | 19 | def needs_redraw? 20 | @state.needs_redraw? 21 | end 22 | 23 | def needs_cursor? 24 | Utils.update_interval > 200 25 | end 26 | 27 | def button_down(id) 28 | @state.button_down(id) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /10-partitioning/main.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'gosu' 4 | require 'gosu_texture_packer' 5 | require 'perlin_noise' 6 | 7 | root_dir = File.dirname(__FILE__) 8 | require_pattern = File.join(root_dir, '**/*.rb') 9 | @failed = [] 10 | 11 | # Dynamically require everything 12 | Dir.glob(require_pattern).each do |f| 13 | next if f.end_with?('_spec.rb') 14 | next if f.end_with?('/main.rb') 15 | begin 16 | require_relative f.gsub("#{root_dir}/", '') 17 | rescue 18 | # May fail if parent class not required yet 19 | @failed << f 20 | end 21 | end 22 | 23 | # Retry unresolved requires 24 | @failed.each do |f| 25 | require_relative f.gsub("#{root_dir}/", '') 26 | end 27 | 28 | $debug = false 29 | $window = GameWindow.new 30 | GameState.switch(MenuState.instance) 31 | $window.show 32 | -------------------------------------------------------------------------------- /10-partitioning/misc/axis_aligned_bounding_box.rb: -------------------------------------------------------------------------------- 1 | class AxisAlignedBoundingBox 2 | attr_reader :center, :half_dimension 3 | def initialize(center, half_dimension) 4 | @center = center 5 | @half_dimension = half_dimension 6 | @dhx = (@half_dimension[0] - @center[0]).abs 7 | @dhy = (@half_dimension[1] - @center[1]).abs 8 | end 9 | 10 | def contains?(point) 11 | return false unless (@center[0] + @dhx) >= point[0] 12 | return false unless (@center[0] - @dhx) <= point[0] 13 | return false unless (@center[1] + @dhy) >= point[1] 14 | return false unless (@center[1] - @dhy) <= point[1] 15 | true 16 | end 17 | 18 | def intersects?(other) 19 | ocx, ocy = other.center 20 | ohx, ohy = other.half_dimension 21 | odhx = (ohx - ocx).abs 22 | return false unless (@center[0] + @dhx) >= (ocx - odhx) 23 | return false unless (@center[0] - @dhx) <= (ocx + odhx) 24 | odhy = (ohy - ocy).abs 25 | return false unless (@center[1] + @dhy) >= (ocy - odhy) 26 | return false unless (@center[1] - @dhy) <= (ocy + odhy) 27 | true 28 | end 29 | 30 | def to_s 31 | "c: #{@center}, h: #{@half_dimension}" 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /10-partitioning/misc/names.rb: -------------------------------------------------------------------------------- 1 | class Names 2 | def initialize(file) 3 | @names = File.read(file).split("\n").reject do |n| 4 | n.size > 12 5 | end 6 | end 7 | 8 | def random 9 | name = @names.sample 10 | @names.delete(name) 11 | name 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /11-powerups/entities/box.rb: -------------------------------------------------------------------------------- 1 | class Box < GameObject 2 | attr_reader :health, :graphics, :angle 3 | 4 | def initialize(object_pool, x, y) 5 | super 6 | @graphics = BoxGraphics.new(self) 7 | @health = Health.new(self, object_pool, 10, true) 8 | @angle = rand(-15..15) 9 | end 10 | 11 | def on_collision(object) 12 | return unless object.physics.speed > 1.0 13 | move(*Utils.point_at_distance(@x, @y, object.direction, 2)) 14 | @box = nil 15 | end 16 | 17 | def box 18 | return @box if @box 19 | w = @graphics.width / 2 20 | h = @graphics.height / 2 21 | # Bounding box adjusted to trim shadows 22 | @box = [x - w + 4, y - h + 8, 23 | x + w , y - h + 8, 24 | x + w , y + h, 25 | x - w + 4, y + h] 26 | @box = Utils.rotate(@angle, @x, @y, *@box) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /11-powerups/entities/bullet.rb: -------------------------------------------------------------------------------- 1 | class Bullet < GameObject 2 | attr_accessor :target_x, :target_y, :source, :speed, :fired_at 3 | 4 | def initialize(object_pool, source_x, source_y, target_x, target_y) 5 | super(object_pool, source_x, source_y) 6 | @target_x, @target_y = target_x, target_y 7 | BulletPhysics.new(self, object_pool) 8 | BulletGraphics.new(self) 9 | BulletSounds.play(self, object_pool.camera) 10 | end 11 | 12 | def box 13 | [@x, @y] 14 | end 15 | 16 | def explode 17 | Explosion.new(object_pool, @x, @y) 18 | mark_for_removal 19 | end 20 | 21 | def fire(source, speed) 22 | @source = source 23 | @speed = speed 24 | @fired_at = Gosu.milliseconds 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /11-powerups/entities/components/ai/tank_chasing_state.rb: -------------------------------------------------------------------------------- 1 | class TankChasingState < TankMotionState 2 | def initialize(object, vision, gun) 3 | super(object, vision) 4 | @object = object 5 | @vision = vision 6 | @gun = gun 7 | end 8 | 9 | def update 10 | change_direction if should_change_direction? 11 | drive 12 | end 13 | 14 | def change_direction 15 | @object.physics.change_direction( 16 | @gun.desired_gun_angle - 17 | @gun.desired_gun_angle % 45) 18 | 19 | @changed_direction_at = Gosu.milliseconds 20 | @will_keep_direction_for = turn_time 21 | end 22 | 23 | def drive_time 24 | 10000 25 | end 26 | 27 | def turn_time 28 | rand(300..600) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /11-powerups/entities/components/ai/tank_fighting_state.rb: -------------------------------------------------------------------------------- 1 | class TankFightingState < TankMotionState 2 | def initialize(object, vision) 3 | super 4 | @object = object 5 | @vision = vision 6 | end 7 | 8 | def update 9 | change_direction if should_change_direction? 10 | if substate_expired? 11 | rand > 0.2 ? drive : wait 12 | end 13 | end 14 | 15 | def change_direction 16 | change = case rand(0..100) 17 | when 0..20 18 | -45 19 | when 20..40 20 | 45 21 | when 40..60 22 | 90 23 | when 60..80 24 | -90 25 | when 80..90 26 | 135 27 | when 90..100 28 | -135 29 | end 30 | @object.physics.change_direction( 31 | @object.direction + change) 32 | @changed_direction_at = Gosu.milliseconds 33 | @will_keep_direction_for = turn_time 34 | end 35 | 36 | def wait_time 37 | rand(300..1000) 38 | end 39 | 40 | def drive_time 41 | rand(2000..5000) 42 | end 43 | 44 | def turn_time 45 | rand(500..2500) 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /11-powerups/entities/components/ai/tank_fleeing_state.rb: -------------------------------------------------------------------------------- 1 | class TankFleeingState < TankMotionState 2 | MAX_FLEE_TIME = 15 * 1000 # 15 seconds 3 | def initialize(object, vision, gun) 4 | super(object, vision) 5 | @object = object 6 | @vision = vision 7 | @gun = gun 8 | end 9 | 10 | def can_flee? 11 | return true unless @started_fleeing 12 | Gosu.milliseconds - @started_fleeing < MAX_FLEE_TIME 13 | end 14 | 15 | def enter 16 | @started_fleeing ||= Gosu.milliseconds 17 | end 18 | 19 | def update 20 | change_direction if should_change_direction? 21 | drive 22 | end 23 | 24 | def change_direction 25 | @object.physics.change_direction( 26 | 180 + @gun.desired_gun_angle - 27 | @gun.desired_gun_angle % 45) 28 | 29 | @changed_direction_at = Gosu.milliseconds 30 | @will_keep_direction_for = turn_time 31 | end 32 | 33 | def drive_time 34 | 10000 35 | end 36 | 37 | def turn_time 38 | rand(300..600) 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /11-powerups/entities/components/ai/tank_roaming_state.rb: -------------------------------------------------------------------------------- 1 | class TankRoamingState < TankMotionState 2 | def initialize(object, vision) 3 | super 4 | @object = object 5 | @vision = vision 6 | end 7 | 8 | def update 9 | change_direction if should_change_direction? 10 | if substate_expired? 11 | rand > 0.3 ? drive : wait 12 | end 13 | end 14 | 15 | def change_direction 16 | change = case rand(0..100) 17 | when 0..30 18 | -45 19 | when 30..60 20 | 45 21 | when 60..70 22 | 90 23 | when 80..90 24 | -90 25 | else 26 | 0 27 | end 28 | if change != 0 29 | @object.physics.change_direction( 30 | @object.direction + change) 31 | end 32 | @changed_direction_at = Gosu.milliseconds 33 | @will_keep_direction_for = turn_time 34 | end 35 | 36 | def wait_time 37 | rand(500..2000) 38 | end 39 | 40 | def drive_time 41 | rand(1000..5000) 42 | end 43 | 44 | def turn_time 45 | rand(2000..5000) 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /11-powerups/entities/components/ai/vision.rb: -------------------------------------------------------------------------------- 1 | class AiVision 2 | CACHE_TIMEOUT = 500 3 | attr_reader :in_sight 4 | 5 | def initialize(viewer, object_pool, distance) 6 | @viewer = viewer 7 | @object_pool = object_pool 8 | @distance = distance 9 | end 10 | 11 | def update 12 | @in_sight = @object_pool.nearby(@viewer, @distance) 13 | end 14 | 15 | def closest_tank 16 | now = Gosu.milliseconds 17 | @closest_tank = nil 18 | if now - (@cache_updated_at ||= 0) > CACHE_TIMEOUT 19 | @closest_tank = nil 20 | @cache_updated_at = now 21 | end 22 | @closest_tank ||= find_closest_tank 23 | end 24 | 25 | private 26 | 27 | def find_closest_tank 28 | @in_sight.select do |o| 29 | o.class == Tank && !o.health.dead? 30 | end.sort do |a, b| 31 | x, y = @viewer.x, @viewer.y 32 | d1 = Utils.distance_between(x, y, a.x, a.y) 33 | d2 = Utils.distance_between(x, y, b.x, b.y) 34 | d1 <=> d2 35 | end.first 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /11-powerups/entities/components/box_graphics.rb: -------------------------------------------------------------------------------- 1 | class BoxGraphics < Component 2 | def initialize(object) 3 | super(object) 4 | load_sprite 5 | end 6 | 7 | def draw(viewport) 8 | @box.draw_rot(x, y, 0, object.angle) 9 | Utils.mark_corners(object.box) if $debug 10 | end 11 | 12 | def height 13 | @box.height 14 | end 15 | 16 | def width 17 | @box.width 18 | end 19 | 20 | private 21 | 22 | def load_sprite 23 | frame = boxes.frame_list.sample 24 | @box = boxes.frame(frame) 25 | end 26 | 27 | def center_x 28 | @center_x ||= x - width / 2 29 | end 30 | 31 | def center_y 32 | @center_y ||= y - height / 2 33 | end 34 | 35 | def boxes 36 | @@boxes ||= Gosu::TexturePacker.load_json($window, 37 | Utils.media_path('boxes_barrels.json')) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /11-powerups/entities/components/bullet_graphics.rb: -------------------------------------------------------------------------------- 1 | class BulletGraphics < Component 2 | def draw(viewport) 3 | image.draw(x - 8, y - 8, 1) 4 | Utils.mark_corners(object.box) if $debug 5 | end 6 | 7 | private 8 | 9 | def image 10 | @@bullet ||= Gosu::Image.new( 11 | $window, Utils.media_path('bullet.png'), false) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /11-powerups/entities/components/bullet_sounds.rb: -------------------------------------------------------------------------------- 1 | class BulletSounds 2 | class << self 3 | def play(object, camera) 4 | volume, pan = Utils.volume_and_pan(object, camera) 5 | sound.play(object.object_id, pan, volume) 6 | end 7 | 8 | private 9 | 10 | def sound 11 | @@sound ||= StereoSample.new( 12 | $window, Utils.media_path('fire.mp3')) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /11-powerups/entities/components/component.rb: -------------------------------------------------------------------------------- 1 | class Component 2 | attr_reader :object # better performance 3 | 4 | def initialize(game_object = nil) 5 | self.object = game_object 6 | end 7 | 8 | def update 9 | # override 10 | end 11 | 12 | def draw(viewport) 13 | # override 14 | end 15 | 16 | protected 17 | 18 | def object=(obj) 19 | if obj 20 | @object = obj 21 | obj.components << self 22 | end 23 | end 24 | 25 | def x 26 | @object.x 27 | end 28 | 29 | def y 30 | @object.y 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /11-powerups/entities/components/damage_graphics.rb: -------------------------------------------------------------------------------- 1 | class DamageGraphics < Component 2 | def initialize(object_pool) 3 | super 4 | @image = images.sample 5 | @angle = rand(0..360) 6 | end 7 | 8 | def draw(viewport) 9 | @image.draw_rot(x, y, 0, @angle) 10 | end 11 | 12 | private 13 | 14 | def images 15 | @@images ||= (1..4).map do |i| 16 | Gosu::Image.new($window, 17 | Utils.media_path("damage#{i}.png"), false) 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /11-powerups/entities/components/explosion_graphics.rb: -------------------------------------------------------------------------------- 1 | class ExplosionGraphics < Component 2 | FRAME_DELAY = 16.66 # ms 3 | 4 | def initialize(game_object) 5 | super 6 | @current_frame = 0 7 | end 8 | 9 | def draw(viewport) 10 | image = current_frame 11 | image.draw( 12 | x - image.width / 2 + 3, 13 | y - image.height / 2 - 35, 14 | 20) 15 | end 16 | 17 | def update 18 | now = Gosu.milliseconds 19 | delta = now - (@last_frame ||= now) 20 | if delta > FRAME_DELAY 21 | @last_frame = now 22 | end 23 | @current_frame += (delta / FRAME_DELAY).floor 24 | object.mark_for_removal if done? 25 | end 26 | 27 | private 28 | 29 | def current_frame 30 | animation[@current_frame % animation.size] 31 | end 32 | 33 | def done? 34 | @done ||= @current_frame >= animation.size 35 | end 36 | 37 | def animation 38 | @@animation ||= 39 | Gosu::Image.load_tiles( 40 | $window, Utils.media_path('explosion.png'), 41 | 128, 128, false) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /11-powerups/entities/components/explosion_sounds.rb: -------------------------------------------------------------------------------- 1 | class ExplosionSounds 2 | class << self 3 | def play(object, camera) 4 | volume, pan = Utils.volume_and_pan(object, camera) 5 | sound.play(object.object_id, pan, volume) 6 | end 7 | 8 | private 9 | 10 | def sound 11 | @@sound ||= StereoSample.new( 12 | $window, Utils.media_path('explosion.mp3')) 13 | end 14 | end 15 | end 16 | 17 | -------------------------------------------------------------------------------- /11-powerups/entities/components/player_sounds.rb: -------------------------------------------------------------------------------- 1 | class PlayerSounds 2 | class << self 3 | def respawn(object, camera) 4 | volume, pan = Utils.volume_and_pan(object, camera) 5 | respawn_sound.play(object.object_id, pan, volume * 0.5) 6 | end 7 | 8 | private 9 | 10 | def respawn_sound 11 | @@respawn ||= StereoSample.new( 12 | $window, Utils.media_path('respawn.wav')) 13 | end 14 | end 15 | end 16 | 17 | -------------------------------------------------------------------------------- /11-powerups/entities/components/powerup_graphics.rb: -------------------------------------------------------------------------------- 1 | class PowerupGraphics < Component 2 | def initialize(object, type) 3 | super(object) 4 | @type = type 5 | end 6 | 7 | def draw(viewport) 8 | image.draw(x - 12, y - 12, 1) 9 | Utils.mark_corners(object.box) if $debug 10 | end 11 | 12 | private 13 | 14 | def image 15 | @image ||= images.frame("#{@type}.png") 16 | end 17 | 18 | def images 19 | @@images ||= Gosu::TexturePacker.load_json( 20 | $window, Utils.media_path('pickups.json')) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /11-powerups/entities/components/powerup_sounds.rb: -------------------------------------------------------------------------------- 1 | class PowerupSounds 2 | class << self 3 | def play(object, camera) 4 | volume, pan = Utils.volume_and_pan(object, camera) 5 | sound.play(object.object_id, pan, volume) 6 | end 7 | 8 | private 9 | 10 | def sound 11 | @@sound ||= StereoSample.new( 12 | $window, Utils.media_path('powerup.mp3')) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /11-powerups/entities/components/tank_graphics.rb: -------------------------------------------------------------------------------- 1 | class TankGraphics < Component 2 | def initialize(game_object) 3 | super(game_object) 4 | @body_normal = units.frame('tank1_body.png') 5 | @shadow_normal = units.frame('tank1_body_shadow.png') 6 | @gun_normal = units.frame('tank1_dualgun.png') 7 | @body_dead = units.frame('tank1_body_destroyed.png') 8 | @shadow_dead = units.frame('tank1_body_destroyed_shadow.png') 9 | @gun_dead = nil 10 | update 11 | end 12 | 13 | def update 14 | if object && object.health.dead? 15 | @body = @body_dead 16 | @gun = @gun_dead 17 | @shadow = @shadow_dead 18 | else 19 | @body = @body_normal 20 | @gun = @gun_normal 21 | @shadow = @shadow_normal 22 | end 23 | end 24 | 25 | def draw(viewport) 26 | @shadow.draw_rot(x - 1, y - 1, 0, object.direction) 27 | @body.draw_rot(x, y, 1, object.direction) 28 | @gun.draw_rot(x, y, 2, object.gun_angle) if @gun 29 | Utils.mark_corners(object.box) if $debug 30 | end 31 | 32 | def width 33 | @body.width 34 | end 35 | 36 | def height 37 | @body.height 38 | end 39 | 40 | private 41 | 42 | def units 43 | @@units = Gosu::TexturePacker.load_json( 44 | $window, Utils.media_path('ground_units.json'), :precise) 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /11-powerups/entities/components/tank_health.rb: -------------------------------------------------------------------------------- 1 | class TankHealth < Health 2 | RESPAWN_DELAY = 5000 3 | attr_accessor :health 4 | 5 | def initialize(object, object_pool) 6 | super(object, object_pool, 100, true) 7 | end 8 | 9 | def should_respawn? 10 | Gosu.milliseconds - @death_time > RESPAWN_DELAY 11 | end 12 | 13 | protected 14 | 15 | def draw? 16 | true 17 | end 18 | 19 | def after_death 20 | object.reset_modifiers 21 | @death_time = Gosu.milliseconds 22 | Thread.new do 23 | sleep(rand(0.1..0.3)) 24 | Explosion.new(@object_pool, x, y) 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /11-powerups/entities/components/tank_sounds.rb: -------------------------------------------------------------------------------- 1 | class TankSounds < Component 2 | def initialize(object, object_pool) 3 | super(object) 4 | @object_pool = object_pool 5 | end 6 | 7 | def update 8 | id = object.object_id 9 | if object.physics.moving? 10 | move_volume = Utils.volume( 11 | object, @object_pool.camera) 12 | pan = Utils.pan(object, @object_pool.camera) 13 | if driving_sound.paused?(id) 14 | driving_sound.resume(id) 15 | elsif driving_sound.stopped?(id) 16 | driving_sound.play(id, pan, 0.5, 1, true) 17 | end 18 | driving_sound.volume_and_pan(id, move_volume * 0.5, pan) 19 | else 20 | if driving_sound.playing?(id) 21 | driving_sound.pause(id) 22 | end 23 | end 24 | end 25 | 26 | def collide 27 | vol, pan = Utils.volume_and_pan( 28 | object, @object_pool.camera) 29 | crash_sound.play(self.object_id, pan, vol, 1, false) 30 | end 31 | 32 | private 33 | 34 | def driving_sound 35 | @@driving_sound ||= StereoSample.new( 36 | $window, Utils.media_path('tank_driving.mp3')) 37 | end 38 | 39 | def crash_sound 40 | @@crash_sound ||= StereoSample.new( 41 | $window, Utils.media_path('metal_interaction2.wav')) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /11-powerups/entities/damage.rb: -------------------------------------------------------------------------------- 1 | class Damage < GameObject 2 | MAX_INSTANCES = 300 3 | @@instances = [] 4 | 5 | def initialize(object_pool, x, y) 6 | super 7 | DamageGraphics.new(self) 8 | track(self) 9 | end 10 | 11 | def effect? 12 | true 13 | end 14 | 15 | private 16 | 17 | def track(instance) 18 | if @@instances.size < MAX_INSTANCES 19 | @@instances << instance 20 | else 21 | out = @@instances.shift 22 | out.mark_for_removal 23 | @@instances << instance 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /11-powerups/entities/explosion.rb: -------------------------------------------------------------------------------- 1 | class Explosion < GameObject 2 | 3 | def initialize(object_pool, x, y) 4 | super 5 | @object_pool = object_pool 6 | if @object_pool.map.can_move_to?(x, y) 7 | Damage.new(@object_pool, x, y) 8 | end 9 | ExplosionGraphics.new(self) 10 | ExplosionSounds.play(self, object_pool.camera) 11 | inflict_damage 12 | end 13 | 14 | def effect? 15 | true 16 | end 17 | 18 | def mark_for_removal 19 | super 20 | end 21 | 22 | private 23 | 24 | def inflict_damage 25 | object_pool.nearby(self, 100).each do |obj| 26 | if obj.respond_to?(:health) 27 | obj.health.inflict_damage( 28 | Math.sqrt(3 * 100 - Utils.distance_between( 29 | obj.x, obj.y, @x, @y))) 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /11-powerups/entities/game_object.rb: -------------------------------------------------------------------------------- 1 | class GameObject 2 | attr_reader :x, :y, :location, :components 3 | def initialize(object_pool, x, y) 4 | @x, @y = x, y 5 | @location = [x, y] 6 | @components = [] 7 | @object_pool = object_pool 8 | @object_pool.add(self) 9 | end 10 | 11 | def move(new_x, new_y) 12 | return if new_x == @x && new_y == @y 13 | @object_pool.tree_remove(self) 14 | @x = new_x 15 | @y = new_y 16 | @location = [new_x, new_y] 17 | @object_pool.tree_insert(self) 18 | end 19 | 20 | def update 21 | @components.map(&:update) 22 | end 23 | 24 | def draw(viewport) 25 | @components.each { |c| c.draw(viewport) } 26 | end 27 | 28 | def removable? 29 | @removable 30 | end 31 | 32 | def mark_for_removal 33 | @removable = true 34 | end 35 | 36 | def on_collision(object) 37 | end 38 | 39 | def effect? 40 | false 41 | end 42 | 43 | def box 44 | end 45 | 46 | def collide 47 | end 48 | 49 | protected 50 | 51 | def object_pool 52 | @object_pool 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /11-powerups/entities/object_pool.rb: -------------------------------------------------------------------------------- 1 | class ObjectPool 2 | attr_accessor :map, :camera, :objects, :powerup_respawn_queue 3 | 4 | def size 5 | @objects.size 6 | end 7 | 8 | def initialize(box) 9 | @tree = QuadTree.new(box) 10 | @powerup_respawn_queue = PowerupRespawnQueue.new 11 | @objects = [] 12 | end 13 | 14 | def add(object) 15 | @objects << object 16 | @tree.insert(object) 17 | end 18 | 19 | def tree_remove(object) 20 | @tree.remove(object) 21 | end 22 | 23 | def tree_insert(object) 24 | @tree.insert(object) 25 | end 26 | 27 | def update_all 28 | @objects.each(&:update) 29 | @objects.reject! do |o| 30 | if o.removable? 31 | @tree.remove(o) 32 | true 33 | end 34 | end 35 | @powerup_respawn_queue.respawn(self) 36 | end 37 | 38 | def nearby(object, max_distance) 39 | cx, cy = object.location 40 | hx, hy = cx + max_distance, cy + max_distance 41 | # Fast, rough results 42 | results = @tree.query_range( 43 | AxisAlignedBoundingBox.new([cx, cy], [hx, hy])) 44 | # Sift through to select fine-grained results 45 | results.select do |o| 46 | o != object && 47 | Utils.distance_between( 48 | o.x, o.y, object.x, object.y) <= max_distance 49 | end 50 | end 51 | 52 | def query_range(box) 53 | @tree.query_range(box) 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /11-powerups/entities/powerups/fire_rate_powerup.rb: -------------------------------------------------------------------------------- 1 | class FireRatePowerup < Powerup 2 | def pickup(object) 3 | if object.class == Tank 4 | if object.fire_rate_modifier < 2 5 | object.fire_rate_modifier += 0.25 6 | end 7 | true 8 | end 9 | end 10 | 11 | def graphics 12 | :straight_gun 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /11-powerups/entities/powerups/health_powerup.rb: -------------------------------------------------------------------------------- 1 | class HealthPowerup < Powerup 2 | def pickup(object) 3 | if object.class == Tank 4 | object.health.increase(25) 5 | true 6 | end 7 | end 8 | 9 | def graphics 10 | :life_up 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /11-powerups/entities/powerups/powerup.rb: -------------------------------------------------------------------------------- 1 | class Powerup < GameObject 2 | def initialize(object_pool, x, y) 3 | super 4 | PowerupGraphics.new(self, graphics) 5 | end 6 | 7 | def box 8 | [x - 8, y - 8, 9 | x + 8, y - 8, 10 | x + 8, y + 8, 11 | x - 8, y + 8] 12 | end 13 | 14 | def on_collision(object) 15 | if pickup(object) 16 | PowerupSounds.play(object, object_pool.camera) 17 | remove 18 | end 19 | end 20 | 21 | def pickup(object) 22 | # override and implement application 23 | end 24 | 25 | def remove 26 | object_pool.powerup_respawn_queue.enqueue( 27 | respawn_delay, 28 | self.class, x, y) 29 | mark_for_removal 30 | end 31 | 32 | def respawn_delay 33 | 30 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /11-powerups/entities/powerups/powerup_respawn_queue.rb: -------------------------------------------------------------------------------- 1 | class PowerupRespawnQueue 2 | RESPAWN_DELAY = 1000 3 | def initialize 4 | @respawn_queue = {} 5 | @last_respawn = Gosu.milliseconds 6 | end 7 | 8 | def enqueue(delay_seconds, type, x, y) 9 | respawn_at = Gosu.milliseconds + delay_seconds * 1000 10 | @respawn_queue[respawn_at.to_i] = [type, x, y] 11 | end 12 | 13 | def respawn(object_pool) 14 | now = Gosu.milliseconds 15 | return if now - @last_respawn < RESPAWN_DELAY 16 | @respawn_queue.keys.each do |k| 17 | next if k > now # not yet 18 | type, x, y = @respawn_queue.delete(k) 19 | type.new(object_pool, x, y) 20 | end 21 | @last_respawn = now 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /11-powerups/entities/powerups/repair_powerup.rb: -------------------------------------------------------------------------------- 1 | class RepairPowerup < Powerup 2 | def pickup(object) 3 | if object.class == Tank 4 | if object.health.health < 100 5 | object.health.restore 6 | end 7 | true 8 | end 9 | end 10 | 11 | def graphics 12 | :repair 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /11-powerups/entities/powerups/tank_speed_powerup.rb: -------------------------------------------------------------------------------- 1 | class TankSpeedPowerup < Powerup 2 | def pickup(object) 3 | if object.class == Tank 4 | if object.speed_modifier < 1.5 5 | object.speed_modifier += 0.10 6 | end 7 | true 8 | end 9 | end 10 | 11 | def graphics 12 | :wingman 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /11-powerups/entities/tree.rb: -------------------------------------------------------------------------------- 1 | class Tree < GameObject 2 | attr_reader :health, :graphics 3 | 4 | def initialize(object_pool, x, y, seed) 5 | super(object_pool, x, y) 6 | @graphics = TreeGraphics.new(self, seed) 7 | @health = Health.new(self, object_pool, 30, false) 8 | @angle = rand(-15..15) 9 | end 10 | 11 | def on_collision(object) 12 | @graphics.shake(object.direction) 13 | end 14 | 15 | def box 16 | [@x, @y] 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /11-powerups/game_states/game_state.rb: -------------------------------------------------------------------------------- 1 | class GameState 2 | 3 | def self.switch(new_state) 4 | $window.state && $window.state.leave 5 | $window.state = new_state 6 | new_state.enter 7 | end 8 | 9 | def enter 10 | end 11 | 12 | def leave 13 | end 14 | 15 | def draw 16 | end 17 | 18 | def update 19 | end 20 | 21 | def needs_redraw? 22 | true 23 | end 24 | 25 | def button_down(id) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /11-powerups/game_window.rb: -------------------------------------------------------------------------------- 1 | class GameWindow < Gosu::Window 2 | attr_accessor :state 3 | 4 | def initialize 5 | super((ENV['w'] || 800).to_i, 6 | (ENV['h'] || 600).to_i, 7 | (ENV['fs'] ? true : false)) 8 | end 9 | 10 | def update 11 | Utils.track_update_interval 12 | @state.update 13 | end 14 | 15 | def draw 16 | @state.draw 17 | end 18 | 19 | def needs_redraw? 20 | @state.needs_redraw? 21 | end 22 | 23 | def needs_cursor? 24 | Utils.update_interval > 200 25 | end 26 | 27 | def button_down(id) 28 | @state.button_down(id) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /11-powerups/main.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'gosu' 4 | require 'gosu_texture_packer' 5 | require 'perlin_noise' 6 | 7 | root_dir = File.dirname(__FILE__) 8 | require_pattern = File.join(root_dir, '**/*.rb') 9 | @failed = [] 10 | 11 | # Dynamically require everything 12 | Dir.glob(require_pattern).each do |f| 13 | next if f.end_with?('_spec.rb') 14 | next if f.end_with?('/main.rb') 15 | begin 16 | require_relative f.gsub("#{root_dir}/", '') 17 | rescue 18 | # May fail if parent class not required yet 19 | @failed << f 20 | end 21 | end 22 | 23 | # Retry unresolved requires 24 | @failed.each do |f| 25 | require_relative f.gsub("#{root_dir}/", '') 26 | end 27 | 28 | $debug = false 29 | $window = GameWindow.new 30 | GameState.switch(MenuState.instance) 31 | $window.show 32 | -------------------------------------------------------------------------------- /11-powerups/misc/axis_aligned_bounding_box.rb: -------------------------------------------------------------------------------- 1 | class AxisAlignedBoundingBox 2 | attr_reader :center, :half_dimension 3 | def initialize(center, half_dimension) 4 | @center = center 5 | @half_dimension = half_dimension 6 | @dhx = (@half_dimension[0] - @center[0]).abs 7 | @dhy = (@half_dimension[1] - @center[1]).abs 8 | end 9 | 10 | def contains?(point) 11 | return false unless (@center[0] + @dhx) >= point[0] 12 | return false unless (@center[0] - @dhx) <= point[0] 13 | return false unless (@center[1] + @dhy) >= point[1] 14 | return false unless (@center[1] - @dhy) <= point[1] 15 | true 16 | end 17 | 18 | def intersects?(other) 19 | ocx, ocy = other.center 20 | ohx, ohy = other.half_dimension 21 | odhx = (ohx - ocx).abs 22 | return false unless (@center[0] + @dhx) >= (ocx - odhx) 23 | return false unless (@center[0] - @dhx) <= (ocx + odhx) 24 | odhy = (ohy - ocy).abs 25 | return false unless (@center[1] + @dhy) >= (ocy - odhy) 26 | return false unless (@center[1] - @dhy) <= (ocy + odhy) 27 | true 28 | end 29 | 30 | def to_s 31 | "c: #{@center}, h: #{@half_dimension}" 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /11-powerups/misc/names.rb: -------------------------------------------------------------------------------- 1 | class Names 2 | def initialize(file) 3 | @names = File.read(file).split("\n").reject do |n| 4 | n.size > 12 5 | end 6 | end 7 | 8 | def random 9 | name = @names.sample 10 | @names.delete(name) 11 | name 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /12-stats/entities/box.rb: -------------------------------------------------------------------------------- 1 | class Box < GameObject 2 | attr_reader :health, :graphics, :angle 3 | 4 | def initialize(object_pool, x, y) 5 | super 6 | @graphics = BoxGraphics.new(self) 7 | @health = Health.new(self, object_pool, 10, true) 8 | @angle = rand(-15..15) 9 | end 10 | 11 | def on_collision(object) 12 | return unless object.physics.speed > 1.0 13 | move(*Utils.point_at_distance(@x, @y, object.direction, 2)) 14 | @box = nil 15 | end 16 | 17 | def box 18 | return @box if @box 19 | w = @graphics.width / 2 20 | h = @graphics.height / 2 21 | # Bounding box adjusted to trim shadows 22 | @box = [x - w + 4, y - h + 8, 23 | x + w , y - h + 8, 24 | x + w , y + h, 25 | x - w + 4, y + h] 26 | @box = Utils.rotate(@angle, @x, @y, *@box) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /12-stats/entities/bullet.rb: -------------------------------------------------------------------------------- 1 | class Bullet < GameObject 2 | attr_accessor :target_x, :target_y, :source, :speed, :fired_at 3 | 4 | def initialize(object_pool, source_x, source_y, target_x, target_y) 5 | super(object_pool, source_x, source_y) 6 | @target_x, @target_y = target_x, target_y 7 | BulletPhysics.new(self, object_pool) 8 | BulletGraphics.new(self) 9 | BulletSounds.play(self, object_pool.camera) 10 | end 11 | 12 | def box 13 | [@x, @y] 14 | end 15 | 16 | def explode 17 | Explosion.new(object_pool, @x, @y, @source) 18 | mark_for_removal 19 | end 20 | 21 | def fire(source, speed) 22 | @source = source 23 | @speed = speed 24 | @fired_at = Gosu.milliseconds 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /12-stats/entities/components/ai/tank_chasing_state.rb: -------------------------------------------------------------------------------- 1 | class TankChasingState < TankMotionState 2 | def initialize(object, vision, gun) 3 | super(object, vision) 4 | @object = object 5 | @vision = vision 6 | @gun = gun 7 | end 8 | 9 | def update 10 | change_direction if should_change_direction? 11 | drive 12 | end 13 | 14 | def change_direction 15 | @object.physics.change_direction( 16 | @gun.desired_gun_angle - 17 | @gun.desired_gun_angle % 45) 18 | 19 | @changed_direction_at = Gosu.milliseconds 20 | @will_keep_direction_for = turn_time 21 | end 22 | 23 | def drive_time 24 | 10000 25 | end 26 | 27 | def turn_time 28 | rand(300..600) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /12-stats/entities/components/ai/tank_fighting_state.rb: -------------------------------------------------------------------------------- 1 | class TankFightingState < TankMotionState 2 | def initialize(object, vision) 3 | super 4 | @object = object 5 | @vision = vision 6 | end 7 | 8 | def update 9 | change_direction if should_change_direction? 10 | if substate_expired? 11 | rand > 0.2 ? drive : wait 12 | end 13 | end 14 | 15 | def change_direction 16 | change = case rand(0..100) 17 | when 0..20 18 | -45 19 | when 20..40 20 | 45 21 | when 40..60 22 | 90 23 | when 60..80 24 | -90 25 | when 80..90 26 | 135 27 | when 90..100 28 | -135 29 | end 30 | @object.physics.change_direction( 31 | @object.direction + change) 32 | @changed_direction_at = Gosu.milliseconds 33 | @will_keep_direction_for = turn_time 34 | end 35 | 36 | def wait_time 37 | rand(300..1000) 38 | end 39 | 40 | def drive_time 41 | rand(2000..5000) 42 | end 43 | 44 | def turn_time 45 | rand(500..2500) 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /12-stats/entities/components/ai/tank_fleeing_state.rb: -------------------------------------------------------------------------------- 1 | class TankFleeingState < TankMotionState 2 | MAX_FLEE_TIME = 15 * 1000 # 15 seconds 3 | def initialize(object, vision, gun) 4 | super(object, vision) 5 | @object = object 6 | @vision = vision 7 | @gun = gun 8 | end 9 | 10 | def can_flee? 11 | return true unless @started_fleeing 12 | Gosu.milliseconds - @started_fleeing < MAX_FLEE_TIME 13 | end 14 | 15 | def enter 16 | @started_fleeing ||= Gosu.milliseconds 17 | end 18 | 19 | def update 20 | change_direction if should_change_direction? 21 | drive 22 | end 23 | 24 | def change_direction 25 | @object.physics.change_direction( 26 | 180 + @gun.desired_gun_angle - 27 | @gun.desired_gun_angle % 45) 28 | 29 | @changed_direction_at = Gosu.milliseconds 30 | @will_keep_direction_for = turn_time 31 | end 32 | 33 | def drive_time 34 | 10000 35 | end 36 | 37 | def turn_time 38 | rand(300..600) 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /12-stats/entities/components/ai/tank_roaming_state.rb: -------------------------------------------------------------------------------- 1 | class TankRoamingState < TankMotionState 2 | def initialize(object, vision) 3 | super 4 | @object = object 5 | @vision = vision 6 | end 7 | 8 | def update 9 | change_direction if should_change_direction? 10 | if substate_expired? 11 | rand > 0.3 ? drive : wait 12 | end 13 | end 14 | 15 | def change_direction 16 | change = case rand(0..100) 17 | when 0..30 18 | -45 19 | when 30..60 20 | 45 21 | when 60..70 22 | 90 23 | when 80..90 24 | -90 25 | else 26 | 0 27 | end 28 | if change != 0 29 | @object.physics.change_direction( 30 | @object.direction + change) 31 | end 32 | @changed_direction_at = Gosu.milliseconds 33 | @will_keep_direction_for = turn_time 34 | end 35 | 36 | def wait_time 37 | rand(500..2000) 38 | end 39 | 40 | def drive_time 41 | rand(1000..5000) 42 | end 43 | 44 | def turn_time 45 | rand(2000..5000) 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /12-stats/entities/components/ai/vision.rb: -------------------------------------------------------------------------------- 1 | class AiVision 2 | CACHE_TIMEOUT = 500 3 | attr_reader :in_sight 4 | 5 | def initialize(viewer, object_pool, distance) 6 | @viewer = viewer 7 | @object_pool = object_pool 8 | @distance = distance 9 | end 10 | 11 | def update 12 | @in_sight = @object_pool.nearby(@viewer, @distance) 13 | end 14 | 15 | def closest_tank 16 | now = Gosu.milliseconds 17 | @closest_tank = nil 18 | if now - (@cache_updated_at ||= 0) > CACHE_TIMEOUT 19 | @closest_tank = nil 20 | @cache_updated_at = now 21 | end 22 | @closest_tank ||= find_closest_tank 23 | end 24 | 25 | private 26 | 27 | def find_closest_tank 28 | @in_sight.select do |o| 29 | o.class == Tank && !o.health.dead? 30 | end.sort do |a, b| 31 | x, y = @viewer.x, @viewer.y 32 | d1 = Utils.distance_between(x, y, a.x, a.y) 33 | d2 = Utils.distance_between(x, y, b.x, b.y) 34 | d1 <=> d2 35 | end.first 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /12-stats/entities/components/box_graphics.rb: -------------------------------------------------------------------------------- 1 | class BoxGraphics < Component 2 | def initialize(object) 3 | super(object) 4 | load_sprite 5 | end 6 | 7 | def draw(viewport) 8 | @box.draw_rot(x, y, 0, object.angle) 9 | Utils.mark_corners(object.box) if $debug 10 | end 11 | 12 | def height 13 | @box.height 14 | end 15 | 16 | def width 17 | @box.width 18 | end 19 | 20 | private 21 | 22 | def load_sprite 23 | frame = boxes.frame_list.sample 24 | @box = boxes.frame(frame) 25 | end 26 | 27 | def center_x 28 | @center_x ||= x - width / 2 29 | end 30 | 31 | def center_y 32 | @center_y ||= y - height / 2 33 | end 34 | 35 | def boxes 36 | @@boxes ||= Gosu::TexturePacker.load_json($window, 37 | Utils.media_path('boxes_barrels.json')) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /12-stats/entities/components/bullet_graphics.rb: -------------------------------------------------------------------------------- 1 | class BulletGraphics < Component 2 | def draw(viewport) 3 | image.draw(x - 8, y - 8, 1) 4 | Utils.mark_corners(object.box) if $debug 5 | end 6 | 7 | private 8 | 9 | def image 10 | @@bullet ||= Gosu::Image.new( 11 | $window, Utils.media_path('bullet.png'), false) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /12-stats/entities/components/bullet_sounds.rb: -------------------------------------------------------------------------------- 1 | class BulletSounds 2 | class << self 3 | def play(object, camera) 4 | volume, pan = Utils.volume_and_pan(object, camera) 5 | sound.play(object.object_id, pan, volume) 6 | end 7 | 8 | private 9 | 10 | def sound 11 | @@sound ||= StereoSample.new( 12 | $window, Utils.media_path('fire.mp3')) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /12-stats/entities/components/component.rb: -------------------------------------------------------------------------------- 1 | class Component 2 | attr_reader :object # better performance 3 | 4 | def initialize(game_object = nil) 5 | self.object = game_object 6 | end 7 | 8 | def update 9 | # override 10 | end 11 | 12 | def draw(viewport) 13 | # override 14 | end 15 | 16 | protected 17 | 18 | def object=(obj) 19 | if obj 20 | @object = obj 21 | obj.components << self 22 | end 23 | end 24 | 25 | def x 26 | @object.x 27 | end 28 | 29 | def y 30 | @object.y 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /12-stats/entities/components/damage_graphics.rb: -------------------------------------------------------------------------------- 1 | class DamageGraphics < Component 2 | def initialize(object_pool) 3 | super 4 | @image = images.sample 5 | @angle = rand(0..360) 6 | end 7 | 8 | def draw(viewport) 9 | @image.draw_rot(x, y, 0, @angle) 10 | end 11 | 12 | private 13 | 14 | def images 15 | @@images ||= (1..4).map do |i| 16 | Gosu::Image.new($window, 17 | Utils.media_path("damage#{i}.png"), false) 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /12-stats/entities/components/explosion_graphics.rb: -------------------------------------------------------------------------------- 1 | class ExplosionGraphics < Component 2 | FRAME_DELAY = 16.66 # ms 3 | 4 | def initialize(game_object) 5 | super 6 | @current_frame = 0 7 | end 8 | 9 | def draw(viewport) 10 | image = current_frame 11 | image.draw( 12 | x - image.width / 2 + 3, 13 | y - image.height / 2 - 35, 14 | 20) 15 | end 16 | 17 | def update 18 | now = Gosu.milliseconds 19 | delta = now - (@last_frame ||= now) 20 | if delta > FRAME_DELAY 21 | @last_frame = now 22 | end 23 | @current_frame += (delta / FRAME_DELAY).floor 24 | object.mark_for_removal if done? 25 | end 26 | 27 | private 28 | 29 | def current_frame 30 | animation[@current_frame % animation.size] 31 | end 32 | 33 | def done? 34 | @done ||= @current_frame >= animation.size 35 | end 36 | 37 | def animation 38 | @@animation ||= 39 | Gosu::Image.load_tiles( 40 | $window, Utils.media_path('explosion.png'), 41 | 128, 128, false) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /12-stats/entities/components/explosion_sounds.rb: -------------------------------------------------------------------------------- 1 | class ExplosionSounds 2 | class << self 3 | def play(object, camera) 4 | volume, pan = Utils.volume_and_pan(object, camera) 5 | sound.play(object.object_id, pan, volume) 6 | end 7 | 8 | private 9 | 10 | def sound 11 | @@sound ||= StereoSample.new( 12 | $window, Utils.media_path('explosion.mp3')) 13 | end 14 | end 15 | end 16 | 17 | -------------------------------------------------------------------------------- /12-stats/entities/components/player_sounds.rb: -------------------------------------------------------------------------------- 1 | class PlayerSounds 2 | class << self 3 | def respawn(object, camera) 4 | volume, pan = Utils.volume_and_pan(object, camera) 5 | respawn_sound.play(object.object_id, pan, volume * 0.5) 6 | end 7 | 8 | private 9 | 10 | def respawn_sound 11 | @@respawn ||= StereoSample.new( 12 | $window, Utils.media_path('respawn.wav')) 13 | end 14 | end 15 | end 16 | 17 | -------------------------------------------------------------------------------- /12-stats/entities/components/powerup_graphics.rb: -------------------------------------------------------------------------------- 1 | class PowerupGraphics < Component 2 | def initialize(object, type) 3 | super(object) 4 | @type = type 5 | end 6 | 7 | def draw(viewport) 8 | image.draw(x - 12, y - 12, 1) 9 | Utils.mark_corners(object.box) if $debug 10 | end 11 | 12 | private 13 | 14 | def image 15 | @image ||= images.frame("#{@type}.png") 16 | end 17 | 18 | def images 19 | @@images ||= Gosu::TexturePacker.load_json( 20 | $window, Utils.media_path('pickups.json')) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /12-stats/entities/components/powerup_sounds.rb: -------------------------------------------------------------------------------- 1 | class PowerupSounds 2 | class << self 3 | def play(object, camera) 4 | volume, pan = Utils.volume_and_pan(object, camera) 5 | sound.play(object.object_id, pan, volume) 6 | end 7 | 8 | private 9 | 10 | def sound 11 | @@sound ||= StereoSample.new( 12 | $window, Utils.media_path('powerup.mp3')) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /12-stats/entities/components/tank_graphics.rb: -------------------------------------------------------------------------------- 1 | class TankGraphics < Component 2 | def initialize(game_object) 3 | super(game_object) 4 | @body_normal = units.frame('tank1_body.png') 5 | @shadow_normal = units.frame('tank1_body_shadow.png') 6 | @gun_normal = units.frame('tank1_dualgun.png') 7 | @body_dead = units.frame('tank1_body_destroyed.png') 8 | @shadow_dead = units.frame('tank1_body_destroyed_shadow.png') 9 | @gun_dead = nil 10 | update 11 | end 12 | 13 | def update 14 | if object && object.health.dead? 15 | @body = @body_dead 16 | @gun = @gun_dead 17 | @shadow = @shadow_dead 18 | else 19 | @body = @body_normal 20 | @gun = @gun_normal 21 | @shadow = @shadow_normal 22 | end 23 | end 24 | 25 | def draw(viewport) 26 | @shadow.draw_rot(x - 1, y - 1, 0, object.direction) 27 | @body.draw_rot(x, y, 1, object.direction) 28 | @gun.draw_rot(x, y, 2, object.gun_angle) if @gun 29 | Utils.mark_corners(object.box) if $debug 30 | end 31 | 32 | def width 33 | @body.width 34 | end 35 | 36 | def height 37 | @body.height 38 | end 39 | 40 | private 41 | 42 | def units 43 | @@units = Gosu::TexturePacker.load_json( 44 | $window, Utils.media_path('ground_units.json'), :precise) 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /12-stats/entities/components/tank_health.rb: -------------------------------------------------------------------------------- 1 | class TankHealth < Health 2 | RESPAWN_DELAY = 5000 3 | attr_accessor :health 4 | 5 | def initialize(object, object_pool) 6 | super(object, object_pool, 100, true) 7 | end 8 | 9 | def should_respawn? 10 | if @death_time 11 | Gosu.milliseconds - @death_time > RESPAWN_DELAY 12 | end 13 | end 14 | 15 | protected 16 | 17 | def draw? 18 | true 19 | end 20 | 21 | def after_death(cause) 22 | @death_time = Gosu.milliseconds 23 | object.reset_modifiers 24 | object.input.stats.add_death 25 | kill = object != cause ? 1 : -1 26 | cause.input.stats.add_kill(kill) 27 | Thread.new do 28 | sleep(rand(0.1..0.3)) 29 | Explosion.new(@object_pool, x, y, cause) 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /12-stats/entities/components/tank_sounds.rb: -------------------------------------------------------------------------------- 1 | class TankSounds < Component 2 | def initialize(object, object_pool) 3 | super(object) 4 | @object_pool = object_pool 5 | end 6 | 7 | def update 8 | id = object.object_id 9 | if object.physics.moving? 10 | move_volume = Utils.volume( 11 | object, @object_pool.camera) 12 | pan = Utils.pan(object, @object_pool.camera) 13 | if driving_sound.paused?(id) 14 | driving_sound.resume(id) 15 | elsif driving_sound.stopped?(id) 16 | driving_sound.play(id, pan, 0.5, 1, true) 17 | end 18 | driving_sound.volume_and_pan(id, move_volume * 0.5, pan) 19 | else 20 | if driving_sound.playing?(id) 21 | driving_sound.pause(id) 22 | end 23 | end 24 | end 25 | 26 | def collide 27 | vol, pan = Utils.volume_and_pan( 28 | object, @object_pool.camera) 29 | crash_sound.play(self.object_id, pan, vol, 1, false) 30 | end 31 | 32 | private 33 | 34 | def driving_sound 35 | @@driving_sound ||= StereoSample.new( 36 | $window, Utils.media_path('tank_driving.mp3')) 37 | end 38 | 39 | def crash_sound 40 | @@crash_sound ||= StereoSample.new( 41 | $window, Utils.media_path('metal_interaction2.wav')) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /12-stats/entities/damage.rb: -------------------------------------------------------------------------------- 1 | class Damage < GameObject 2 | MAX_INSTANCES = 300 3 | @@instances = [] 4 | 5 | def initialize(object_pool, x, y) 6 | super 7 | DamageGraphics.new(self) 8 | track(self) 9 | end 10 | 11 | def effect? 12 | true 13 | end 14 | 15 | private 16 | 17 | def track(instance) 18 | if @@instances.size < MAX_INSTANCES 19 | @@instances << instance 20 | else 21 | out = @@instances.shift 22 | out.mark_for_removal 23 | @@instances << instance 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /12-stats/entities/explosion.rb: -------------------------------------------------------------------------------- 1 | class Explosion < GameObject 2 | 3 | def initialize(object_pool, x, y, source) 4 | super(object_pool, x, y) 5 | @source = source 6 | @object_pool = object_pool 7 | if @object_pool.map.can_move_to?(x, y) 8 | Damage.new(@object_pool, x, y) 9 | end 10 | ExplosionGraphics.new(self) 11 | ExplosionSounds.play(self, object_pool.camera) 12 | inflict_damage 13 | end 14 | 15 | def effect? 16 | true 17 | end 18 | 19 | def mark_for_removal 20 | super 21 | end 22 | 23 | private 24 | 25 | def inflict_damage 26 | object_pool.nearby(self, 100).each do |obj| 27 | if obj.respond_to?(:health) 28 | obj.health.inflict_damage( 29 | Math.sqrt(3 * 100 - Utils.distance_between( 30 | obj.x, obj.y, @x, @y)), @source) 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /12-stats/entities/game_object.rb: -------------------------------------------------------------------------------- 1 | class GameObject 2 | attr_reader :x, :y, :location, :components 3 | def initialize(object_pool, x, y) 4 | @x, @y = x, y 5 | @location = [x, y] 6 | @components = [] 7 | @object_pool = object_pool 8 | @object_pool.add(self) 9 | end 10 | 11 | def move(new_x, new_y) 12 | return if new_x == @x && new_y == @y 13 | @object_pool.tree_remove(self) 14 | @x = new_x 15 | @y = new_y 16 | @location = [new_x, new_y] 17 | @object_pool.tree_insert(self) 18 | end 19 | 20 | def update 21 | @components.map(&:update) 22 | end 23 | 24 | def draw(viewport) 25 | @components.each { |c| c.draw(viewport) } 26 | end 27 | 28 | def removable? 29 | @removable 30 | end 31 | 32 | def mark_for_removal 33 | @removable = true 34 | end 35 | 36 | def on_collision(object) 37 | end 38 | 39 | def effect? 40 | false 41 | end 42 | 43 | def box 44 | end 45 | 46 | def collide 47 | end 48 | 49 | protected 50 | 51 | def object_pool 52 | @object_pool 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /12-stats/entities/object_pool.rb: -------------------------------------------------------------------------------- 1 | class ObjectPool 2 | attr_accessor :map, :camera, :objects, :powerup_respawn_queue 3 | 4 | def size 5 | @objects.size 6 | end 7 | 8 | def initialize(box) 9 | @tree = QuadTree.new(box) 10 | @powerup_respawn_queue = PowerupRespawnQueue.new 11 | @objects = [] 12 | end 13 | 14 | def add(object) 15 | @objects << object 16 | @tree.insert(object) 17 | end 18 | 19 | def tree_remove(object) 20 | @tree.remove(object) 21 | end 22 | 23 | def tree_insert(object) 24 | @tree.insert(object) 25 | end 26 | 27 | def update_all 28 | @objects.each(&:update) 29 | @objects.reject! do |o| 30 | if o.removable? 31 | @tree.remove(o) 32 | true 33 | end 34 | end 35 | @powerup_respawn_queue.respawn(self) 36 | end 37 | 38 | def nearby(object, max_distance) 39 | cx, cy = object.location 40 | hx, hy = cx + max_distance, cy + max_distance 41 | # Fast, rough results 42 | results = @tree.query_range( 43 | AxisAlignedBoundingBox.new([cx, cy], [hx, hy])) 44 | # Sift through to select fine-grained results 45 | results.select do |o| 46 | o != object && 47 | Utils.distance_between( 48 | o.x, o.y, object.x, object.y) <= max_distance 49 | end 50 | end 51 | 52 | def query_range(box) 53 | @tree.query_range(box) 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /12-stats/entities/powerups/fire_rate_powerup.rb: -------------------------------------------------------------------------------- 1 | class FireRatePowerup < Powerup 2 | def pickup(object) 3 | if object.class == Tank 4 | if object.fire_rate_modifier < 2 5 | object.fire_rate_modifier += 0.25 6 | end 7 | true 8 | end 9 | end 10 | 11 | def graphics 12 | :straight_gun 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /12-stats/entities/powerups/health_powerup.rb: -------------------------------------------------------------------------------- 1 | class HealthPowerup < Powerup 2 | def pickup(object) 3 | if object.class == Tank 4 | object.health.increase(25) 5 | true 6 | end 7 | end 8 | 9 | def graphics 10 | :life_up 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /12-stats/entities/powerups/powerup.rb: -------------------------------------------------------------------------------- 1 | class Powerup < GameObject 2 | def initialize(object_pool, x, y) 3 | super 4 | PowerupGraphics.new(self, graphics) 5 | end 6 | 7 | def box 8 | [x - 8, y - 8, 9 | x + 8, y - 8, 10 | x + 8, y + 8, 11 | x - 8, y + 8] 12 | end 13 | 14 | def on_collision(object) 15 | if pickup(object) 16 | PowerupSounds.play(object, object_pool.camera) 17 | remove 18 | end 19 | end 20 | 21 | def pickup(object) 22 | # override and implement application 23 | end 24 | 25 | def remove 26 | object_pool.powerup_respawn_queue.enqueue( 27 | respawn_delay, 28 | self.class, x, y) 29 | mark_for_removal 30 | end 31 | 32 | def respawn_delay 33 | 30 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /12-stats/entities/powerups/powerup_respawn_queue.rb: -------------------------------------------------------------------------------- 1 | class PowerupRespawnQueue 2 | RESPAWN_DELAY = 1000 3 | def initialize 4 | @respawn_queue = {} 5 | @last_respawn = Gosu.milliseconds 6 | end 7 | 8 | def enqueue(delay_seconds, type, x, y) 9 | respawn_at = Gosu.milliseconds + delay_seconds * 1000 10 | @respawn_queue[respawn_at.to_i] = [type, x, y] 11 | end 12 | 13 | def respawn(object_pool) 14 | now = Gosu.milliseconds 15 | return if now - @last_respawn < RESPAWN_DELAY 16 | @respawn_queue.keys.each do |k| 17 | next if k > now # not yet 18 | type, x, y = @respawn_queue.delete(k) 19 | type.new(object_pool, x, y) 20 | end 21 | @last_respawn = now 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /12-stats/entities/powerups/repair_powerup.rb: -------------------------------------------------------------------------------- 1 | class RepairPowerup < Powerup 2 | def pickup(object) 3 | if object.class == Tank 4 | if object.health.health < 100 5 | object.health.restore 6 | end 7 | true 8 | end 9 | end 10 | 11 | def graphics 12 | :repair 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /12-stats/entities/powerups/tank_speed_powerup.rb: -------------------------------------------------------------------------------- 1 | class TankSpeedPowerup < Powerup 2 | def pickup(object) 3 | if object.class == Tank 4 | if object.speed_modifier < 1.5 5 | object.speed_modifier += 0.10 6 | end 7 | true 8 | end 9 | end 10 | 11 | def graphics 12 | :wingman 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /12-stats/entities/score_display.rb: -------------------------------------------------------------------------------- 1 | class ScoreDisplay 2 | def initialize(object_pool) 3 | tanks = object_pool.objects.select do |o| 4 | o.class == Tank 5 | end 6 | stats = tanks.map(&:input).map(&:stats) 7 | stats.sort! do |stat1, stat2| 8 | stat2.kills <=> stat1.kills 9 | end 10 | create_stats_image(stats) 11 | end 12 | 13 | def create_stats_image(stats) 14 | text = stats.map do |stat| 15 | "#{stat.kills}: #{stat.name} " 16 | end.join("\n") 17 | @stats_image = Gosu::Image.from_text( 18 | $window, text, Utils.main_font, 30) 19 | end 20 | 21 | def draw 22 | @stats_image.draw( 23 | $window.width / 2 - @stats_image.width / 2, 24 | $window.height / 4 + 30, 25 | 1000) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /12-stats/entities/tree.rb: -------------------------------------------------------------------------------- 1 | class Tree < GameObject 2 | attr_reader :health, :graphics 3 | 4 | def initialize(object_pool, x, y, seed) 5 | super(object_pool, x, y) 6 | @graphics = TreeGraphics.new(self, seed) 7 | @health = Health.new(self, object_pool, 30, false) 8 | @angle = rand(-15..15) 9 | end 10 | 11 | def on_collision(object) 12 | @graphics.shake(object.direction) 13 | end 14 | 15 | def box 16 | [@x, @y] 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /12-stats/game_states/game_state.rb: -------------------------------------------------------------------------------- 1 | class GameState 2 | 3 | def self.switch(new_state) 4 | $window.state && $window.state.leave 5 | $window.state = new_state 6 | new_state.enter 7 | end 8 | 9 | def enter 10 | end 11 | 12 | def leave 13 | end 14 | 15 | def draw 16 | end 17 | 18 | def update 19 | end 20 | 21 | def needs_redraw? 22 | true 23 | end 24 | 25 | def button_down(id) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /12-stats/game_states/menu_state.rb: -------------------------------------------------------------------------------- 1 | require 'singleton' 2 | class MenuState < GameState 3 | include Singleton 4 | attr_accessor :play_state 5 | 6 | def initialize 7 | @message = Gosu::Image.from_text( 8 | $window, "Tanks Prototype", 9 | Utils.title_font, 60) 10 | end 11 | 12 | def enter 13 | music.play(true) 14 | music.volume = 1 15 | end 16 | 17 | def leave 18 | music.volume = 0 19 | music.stop 20 | end 21 | 22 | def music 23 | @@music ||= Gosu::Song.new( 24 | $window, Utils.media_path('menu_music.mp3')) 25 | end 26 | 27 | def update 28 | @info = Gosu::Image.from_text( 29 | $window, "Q: Quit\nN: New Game", 30 | Utils.main_font, 30) 31 | end 32 | 33 | def draw 34 | @message.draw( 35 | $window.width / 2 - @message.width / 2, 36 | $window.height / 2 - @message.height / 2, 37 | 10) 38 | @info.draw( 39 | $window.width / 2 - @info.width / 2, 40 | $window.height / 2 - @info.height / 2 + 100, 41 | 10) 42 | end 43 | 44 | def button_down(id) 45 | $window.close if id == Gosu::KbQ 46 | if id == Gosu::KbC && @play_state 47 | GameState.switch(@play_state) 48 | end 49 | if id == Gosu::KbN 50 | @play_state = PlayState.new 51 | GameState.switch(@play_state) 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /12-stats/game_states/pause_state.rb: -------------------------------------------------------------------------------- 1 | require 'singleton' 2 | class PauseState < GameState 3 | include Singleton 4 | attr_accessor :play_state 5 | 6 | def initialize 7 | @message = Gosu::Image.from_text( 8 | $window, "Game Paused", 9 | Utils.title_font, 60) 10 | end 11 | 12 | def enter 13 | music.play(true) 14 | music.volume = 1 15 | @score_display = ScoreDisplay.new(@play_state.object_pool) 16 | @mouse_coords = [$window.mouse_x, $window.mouse_y] 17 | end 18 | 19 | def leave 20 | music.volume = 0 21 | music.stop 22 | $window.mouse_x, $window.mouse_y = @mouse_coords 23 | end 24 | 25 | def music 26 | @@music ||= Gosu::Song.new( 27 | $window, Utils.media_path('menu_music.mp3')) 28 | end 29 | 30 | def draw 31 | @play_state.draw 32 | @message.draw( 33 | $window.width / 2 - @message.width / 2, 34 | $window.height / 4 - @message.height, 35 | 1000) 36 | @score_display.draw 37 | end 38 | 39 | def button_down(id) 40 | $window.close if id == Gosu::KbQ 41 | if id == Gosu::KbC && @play_state 42 | GameState.switch(@play_state) 43 | end 44 | if id == Gosu::KbEscape 45 | GameState.switch(@play_state) 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /12-stats/main.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'gosu' 4 | require 'gosu_texture_packer' 5 | require 'perlin_noise' 6 | 7 | root_dir = File.dirname(__FILE__) 8 | require_pattern = File.join(root_dir, '**/*.rb') 9 | @failed = [] 10 | 11 | # Dynamically require everything 12 | Dir.glob(require_pattern).each do |f| 13 | next if f.end_with?('_spec.rb') 14 | next if f.end_with?('/main.rb') 15 | begin 16 | require_relative f.gsub("#{root_dir}/", '') 17 | rescue 18 | # May fail if parent class not required yet 19 | @failed << f 20 | end 21 | end 22 | 23 | # Retry unresolved requires 24 | @failed.each do |f| 25 | require_relative f.gsub("#{root_dir}/", '') 26 | end 27 | 28 | $debug = false 29 | $window = GameWindow.new 30 | GameState.switch(MenuState.instance) 31 | $window.show 32 | -------------------------------------------------------------------------------- /12-stats/misc/axis_aligned_bounding_box.rb: -------------------------------------------------------------------------------- 1 | class AxisAlignedBoundingBox 2 | attr_reader :center, :half_dimension 3 | def initialize(center, half_dimension) 4 | @center = center 5 | @half_dimension = half_dimension 6 | @dhx = (@half_dimension[0] - @center[0]).abs 7 | @dhy = (@half_dimension[1] - @center[1]).abs 8 | end 9 | 10 | def contains?(point) 11 | return false unless (@center[0] + @dhx) >= point[0] 12 | return false unless (@center[0] - @dhx) <= point[0] 13 | return false unless (@center[1] + @dhy) >= point[1] 14 | return false unless (@center[1] - @dhy) <= point[1] 15 | true 16 | end 17 | 18 | def intersects?(other) 19 | ocx, ocy = other.center 20 | ohx, ohy = other.half_dimension 21 | odhx = (ohx - ocx).abs 22 | return false unless (@center[0] + @dhx) >= (ocx - odhx) 23 | return false unless (@center[0] - @dhx) <= (ocx + odhx) 24 | odhy = (ohy - ocy).abs 25 | return false unless (@center[1] + @dhy) >= (ocy - odhy) 26 | return false unless (@center[1] - @dhy) <= (ocy + odhy) 27 | true 28 | end 29 | 30 | def to_s 31 | "c: #{@center}, h: #{@half_dimension}" 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /12-stats/misc/game_window.rb: -------------------------------------------------------------------------------- 1 | class GameWindow < Gosu::Window 2 | attr_accessor :state 3 | 4 | def initialize 5 | super((ENV['w'] || 800).to_i, 6 | (ENV['h'] || 600).to_i, 7 | (ENV['fs'] ? true : false)) 8 | end 9 | 10 | def update 11 | Utils.track_update_interval 12 | @state.update 13 | end 14 | 15 | def draw 16 | @state.draw 17 | end 18 | 19 | def needs_redraw? 20 | @state.needs_redraw? 21 | end 22 | 23 | def needs_cursor? 24 | Utils.update_interval > 200 25 | end 26 | 27 | def button_down(id) 28 | @state.button_down(id) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /12-stats/misc/names.rb: -------------------------------------------------------------------------------- 1 | class Names 2 | def initialize(file) 3 | @names = File.read(file).split("\n").reject do |n| 4 | n.size > 12 5 | end 6 | end 7 | 8 | def random 9 | name = @names.sample 10 | @names.delete(name) 11 | name 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /12-stats/misc/stats.rb: -------------------------------------------------------------------------------- 1 | class Stats 2 | attr_reader :name, :kills, :deaths, :shots, :changed_at 3 | def initialize(name) 4 | @name = name 5 | @kills = @deaths = @shots = @damage = @damage_dealt = 0 6 | changed 7 | end 8 | 9 | def add_kill(amount = 1) 10 | @kills += amount 11 | changed 12 | end 13 | 14 | def add_death 15 | @deaths += 1 16 | changed 17 | end 18 | 19 | def add_shot 20 | @shots += 1 21 | changed 22 | end 23 | 24 | def add_damage(amount) 25 | @damage += amount 26 | changed 27 | end 28 | 29 | def damage 30 | @damage.round 31 | end 32 | 33 | def add_damage_dealt(amount) 34 | @damage_dealt += amount 35 | changed 36 | end 37 | 38 | def damage_dealt 39 | @damage_dealt.round 40 | end 41 | 42 | def to_s 43 | "[kills: #{@kills}, " \ 44 | "deaths: #{@deaths}, " \ 45 | "shots: #{@shots}, " \ 46 | "damage: #{damage}, " \ 47 | "damage_dealt: #{damage_dealt}]" 48 | end 49 | 50 | private 51 | 52 | def changed 53 | @changed_at = Gosu.milliseconds 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /13-advanced-ai/entities/box.rb: -------------------------------------------------------------------------------- 1 | class Box < GameObject 2 | attr_reader :health, :graphics, :angle 3 | 4 | def initialize(object_pool, x, y) 5 | super 6 | @graphics = BoxGraphics.new(self) 7 | @health = Health.new(self, object_pool, 10, true) 8 | @angle = rand(-15..15) 9 | end 10 | 11 | def on_collision(object) 12 | return unless object.physics.speed > 1.0 13 | move(*Utils.point_at_distance(@x, @y, object.direction, 2)) 14 | @box = nil 15 | end 16 | 17 | def box 18 | return @box if @box 19 | w = @graphics.width / 2 20 | h = @graphics.height / 2 21 | # Bounding box adjusted to trim shadows 22 | @box = [x - w + 4, y - h + 8, 23 | x + w , y - h + 8, 24 | x + w , y + h, 25 | x - w + 4, y + h] 26 | @box = Utils.rotate(@angle, @x, @y, *@box) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /13-advanced-ai/entities/bullet.rb: -------------------------------------------------------------------------------- 1 | class Bullet < GameObject 2 | attr_accessor :target_x, :target_y, :source, :speed, :fired_at 3 | 4 | def initialize(object_pool, source_x, source_y, target_x, target_y) 5 | super(object_pool, source_x, source_y) 6 | @target_x, @target_y = target_x, target_y 7 | BulletPhysics.new(self, object_pool) 8 | BulletGraphics.new(self) 9 | BulletSounds.play(self, object_pool.camera) 10 | end 11 | 12 | def box 13 | [@x, @y] 14 | end 15 | 16 | def explode 17 | Explosion.new(object_pool, @x, @y, @source) 18 | mark_for_removal 19 | end 20 | 21 | def fire(source, speed) 22 | @source = source 23 | @speed = speed 24 | @fired_at = Gosu.milliseconds 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /13-advanced-ai/entities/components/ai/tank_chasing_state.rb: -------------------------------------------------------------------------------- 1 | class TankChasingState < TankMotionState 2 | def initialize(object, vision, gun) 3 | super(object, vision) 4 | @object = object 5 | @vision = vision 6 | @gun = gun 7 | end 8 | 9 | def update 10 | change_direction if should_change_direction? 11 | drive 12 | end 13 | 14 | def change_direction 15 | @object.physics.change_direction( 16 | @gun.desired_gun_angle - 17 | @gun.desired_gun_angle % 45) 18 | 19 | @changed_direction_at = Gosu.milliseconds 20 | @will_keep_direction_for = turn_time 21 | end 22 | 23 | def drive_time 24 | 10000 25 | end 26 | 27 | def turn_time 28 | rand(300..600) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /13-advanced-ai/entities/components/ai/tank_fighting_state.rb: -------------------------------------------------------------------------------- 1 | class TankFightingState < TankMotionState 2 | def initialize(object, vision) 3 | super 4 | @object = object 5 | @vision = vision 6 | end 7 | 8 | def update 9 | change_direction if should_change_direction? 10 | if substate_expired? 11 | rand > 0.1 ? drive : wait 12 | end 13 | end 14 | 15 | def change_direction 16 | change = case rand(0..100) 17 | when 0..20 18 | -45 19 | when 20..40 20 | 45 21 | when 40..60 22 | 90 23 | when 60..80 24 | -90 25 | when 80..90 26 | 135 27 | when 90..100 28 | -135 29 | end 30 | @object.physics.change_direction( 31 | @object.direction + change) 32 | @changed_direction_at = Gosu.milliseconds 33 | @will_keep_direction_for = turn_time 34 | end 35 | 36 | def wait_time 37 | rand(50..300) 38 | end 39 | 40 | def drive_time 41 | rand(5000..10000) 42 | end 43 | 44 | def turn_time 45 | rand(300..3000) 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /13-advanced-ai/entities/components/ai/tank_fleeing_state.rb: -------------------------------------------------------------------------------- 1 | class TankFleeingState < TankMotionState 2 | MAX_FLEE_TIME = 15 * 1000 # 15 seconds 3 | 4 | def initialize(object, vision, gun) 5 | super(object, vision) 6 | @object = object 7 | @vision = vision 8 | @gun = gun 9 | end 10 | 11 | def can_flee? 12 | return true unless @started_fleeing 13 | Gosu.milliseconds - @started_fleeing < MAX_FLEE_TIME 14 | end 15 | 16 | def enter 17 | @started_fleeing ||= Gosu.milliseconds 18 | end 19 | 20 | def update 21 | change_direction if should_change_direction? 22 | drive 23 | end 24 | 25 | def change_direction 26 | closest_powerup = @vision.closest_powerup( 27 | RepairPowerup, HealthPowerup) 28 | if closest_powerup 29 | angle = Utils.angle_between( 30 | @object.x, @object.y, 31 | closest_powerup.x, closest_powerup.y) 32 | @object.physics.change_direction( 33 | angle - angle % 45) 34 | else 35 | @object.physics.change_direction( 36 | 180 + @gun.desired_gun_angle - 37 | @gun.desired_gun_angle % 45) 38 | end 39 | @changed_direction_at = Gosu.milliseconds 40 | @will_keep_direction_for = turn_time 41 | end 42 | 43 | def drive_time 44 | 10000 45 | end 46 | 47 | def turn_time 48 | rand(300..600) 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /13-advanced-ai/entities/components/ai/tank_navigating_state.rb: -------------------------------------------------------------------------------- 1 | class TankNavigatingState < TankMotionState 2 | def initialize(object, vision) 3 | @object = object 4 | @vision = vision 5 | end 6 | 7 | def update 8 | change_direction if should_change_direction? 9 | drive 10 | end 11 | 12 | def change_direction 13 | closest_free_path = @vision.closest_free_path 14 | if closest_free_path 15 | @object.physics.change_direction( 16 | Utils.angle_between( 17 | @object.x, @object.y, *closest_free_path)) 18 | end 19 | @changed_direction_at = Gosu.milliseconds 20 | @will_keep_direction_for = turn_time 21 | end 22 | 23 | def wait_time 24 | rand(10..100) 25 | end 26 | 27 | def drive_time 28 | rand(1000..2000) 29 | end 30 | 31 | def turn_time 32 | rand(300..1000) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /13-advanced-ai/entities/components/ai/tank_stuck_state.rb: -------------------------------------------------------------------------------- 1 | class TankStuckState < TankMotionState 2 | attr_accessor :stuck_at 3 | def initialize(object, vision, gun) 4 | super(object, vision) 5 | @object = object 6 | @vision = vision 7 | @gun = gun 8 | end 9 | 10 | def update 11 | change_direction if should_change_direction? 12 | drive 13 | end 14 | 15 | def change_direction 16 | closest_free_path = @vision.closest_free_path_away_from( 17 | @stuck_at) 18 | if closest_free_path 19 | @object.physics.change_direction( 20 | Utils.angle_between( 21 | @object.x, @object.y, *closest_free_path)) 22 | else 23 | if @object.health.health > 50 && rand > 0.9 24 | @object.shoot(*Utils.point_at_distance( 25 | *@object.location, 26 | @object.gun_angle, 27 | 150)) 28 | end 29 | end 30 | @changed_direction_at = Gosu.milliseconds 31 | @will_keep_direction_for = turn_time 32 | end 33 | 34 | def wait_time 35 | rand(10..100) 36 | end 37 | 38 | def drive_time 39 | rand(1000..2000) 40 | end 41 | 42 | def turn_time 43 | rand(1000..2000) 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /13-advanced-ai/entities/components/box_graphics.rb: -------------------------------------------------------------------------------- 1 | class BoxGraphics < Component 2 | def initialize(object) 3 | super(object) 4 | load_sprite 5 | end 6 | 7 | def draw(viewport) 8 | @box.draw_rot(x, y, 0, object.angle) 9 | Utils.mark_corners(object.box) if $debug 10 | end 11 | 12 | def height 13 | @box.height 14 | end 15 | 16 | def width 17 | @box.width 18 | end 19 | 20 | private 21 | 22 | def load_sprite 23 | frame = boxes.frame_list.sample 24 | @box = boxes.frame(frame) 25 | end 26 | 27 | def center_x 28 | @center_x ||= x - width / 2 29 | end 30 | 31 | def center_y 32 | @center_y ||= y - height / 2 33 | end 34 | 35 | def boxes 36 | @@boxes ||= Gosu::TexturePacker.load_json($window, 37 | Utils.media_path('boxes_barrels.json')) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /13-advanced-ai/entities/components/bullet_graphics.rb: -------------------------------------------------------------------------------- 1 | class BulletGraphics < Component 2 | def draw(viewport) 3 | image.draw(x - 8, y - 8, 1) 4 | Utils.mark_corners(object.box) if $debug 5 | end 6 | 7 | private 8 | 9 | def image 10 | @@bullet ||= Gosu::Image.new( 11 | $window, Utils.media_path('bullet.png'), false) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /13-advanced-ai/entities/components/bullet_sounds.rb: -------------------------------------------------------------------------------- 1 | class BulletSounds 2 | class << self 3 | def play(object, camera) 4 | volume, pan = Utils.volume_and_pan(object, camera) 5 | sound.play(object.object_id, pan, volume) 6 | end 7 | 8 | private 9 | 10 | def sound 11 | @@sound ||= StereoSample.new( 12 | $window, Utils.media_path('fire.mp3')) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /13-advanced-ai/entities/components/component.rb: -------------------------------------------------------------------------------- 1 | class Component 2 | attr_reader :object # better performance 3 | 4 | def initialize(game_object = nil) 5 | self.object = game_object 6 | end 7 | 8 | def update 9 | # override 10 | end 11 | 12 | def draw(viewport) 13 | # override 14 | end 15 | 16 | protected 17 | 18 | def object=(obj) 19 | if obj 20 | @object = obj 21 | obj.components << self 22 | end 23 | end 24 | 25 | def x 26 | @object.x 27 | end 28 | 29 | def y 30 | @object.y 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /13-advanced-ai/entities/components/damage_graphics.rb: -------------------------------------------------------------------------------- 1 | class DamageGraphics < Component 2 | def initialize(object_pool) 3 | super 4 | @image = images.sample 5 | @angle = rand(0..360) 6 | end 7 | 8 | def draw(viewport) 9 | @image.draw_rot(x, y, 0, @angle) 10 | end 11 | 12 | private 13 | 14 | def images 15 | @@images ||= (1..4).map do |i| 16 | Gosu::Image.new($window, 17 | Utils.media_path("damage#{i}.png"), false) 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /13-advanced-ai/entities/components/explosion_graphics.rb: -------------------------------------------------------------------------------- 1 | class ExplosionGraphics < Component 2 | FRAME_DELAY = 16.66 # ms 3 | 4 | def initialize(game_object) 5 | super 6 | @current_frame = 0 7 | end 8 | 9 | def draw(viewport) 10 | image = current_frame 11 | image.draw( 12 | x - image.width / 2 + 3, 13 | y - image.height / 2 - 35, 14 | 20) 15 | end 16 | 17 | def update 18 | now = Gosu.milliseconds 19 | delta = now - (@last_frame ||= now) 20 | if delta > FRAME_DELAY 21 | @last_frame = now 22 | end 23 | @current_frame += (delta / FRAME_DELAY).floor 24 | object.mark_for_removal if done? 25 | end 26 | 27 | private 28 | 29 | def current_frame 30 | animation[@current_frame % animation.size] 31 | end 32 | 33 | def done? 34 | @done ||= @current_frame >= animation.size 35 | end 36 | 37 | def animation 38 | @@animation ||= 39 | Gosu::Image.load_tiles( 40 | $window, Utils.media_path('explosion.png'), 41 | 128, 128, false) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /13-advanced-ai/entities/components/explosion_sounds.rb: -------------------------------------------------------------------------------- 1 | class ExplosionSounds 2 | class << self 3 | def play(object, camera) 4 | volume, pan = Utils.volume_and_pan(object, camera) 5 | sound.play(object.object_id, pan, volume) 6 | end 7 | 8 | private 9 | 10 | def sound 11 | @@sound ||= StereoSample.new( 12 | $window, Utils.media_path('explosion.mp3')) 13 | end 14 | end 15 | end 16 | 17 | -------------------------------------------------------------------------------- /13-advanced-ai/entities/components/player_sounds.rb: -------------------------------------------------------------------------------- 1 | class PlayerSounds 2 | class << self 3 | def respawn(object, camera) 4 | volume, pan = Utils.volume_and_pan(object, camera) 5 | respawn_sound.play(object.object_id, pan, volume * 0.5) 6 | end 7 | 8 | private 9 | 10 | def respawn_sound 11 | @@respawn ||= StereoSample.new( 12 | $window, Utils.media_path('respawn.wav')) 13 | end 14 | end 15 | end 16 | 17 | -------------------------------------------------------------------------------- /13-advanced-ai/entities/components/powerup_graphics.rb: -------------------------------------------------------------------------------- 1 | class PowerupGraphics < Component 2 | def initialize(object, type) 3 | super(object) 4 | @type = type 5 | end 6 | 7 | def draw(viewport) 8 | image.draw(x - 12, y - 12, 1) 9 | Utils.mark_corners(object.box) if $debug 10 | end 11 | 12 | private 13 | 14 | def image 15 | @image ||= images.frame("#{@type}.png") 16 | end 17 | 18 | def images 19 | @@images ||= Gosu::TexturePacker.load_json( 20 | $window, Utils.media_path('pickups.json')) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /13-advanced-ai/entities/components/powerup_sounds.rb: -------------------------------------------------------------------------------- 1 | class PowerupSounds 2 | class << self 3 | def play(object, camera) 4 | volume, pan = Utils.volume_and_pan(object, camera) 5 | sound.play(object.object_id, pan, volume) 6 | end 7 | 8 | private 9 | 10 | def sound 11 | @@sound ||= StereoSample.new( 12 | $window, Utils.media_path('powerup.mp3')) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /13-advanced-ai/entities/components/tank_graphics.rb: -------------------------------------------------------------------------------- 1 | class TankGraphics < Component 2 | def initialize(game_object) 3 | super(game_object) 4 | @body_normal = units.frame('tank1_body.png') 5 | @shadow_normal = units.frame('tank1_body_shadow.png') 6 | @gun_normal = units.frame('tank1_dualgun.png') 7 | @body_dead = units.frame('tank1_body_destroyed.png') 8 | @shadow_dead = units.frame('tank1_body_destroyed_shadow.png') 9 | @gun_dead = nil 10 | update 11 | end 12 | 13 | def update 14 | if object && object.health.dead? 15 | @body = @body_dead 16 | @gun = @gun_dead 17 | @shadow = @shadow_dead 18 | else 19 | @body = @body_normal 20 | @gun = @gun_normal 21 | @shadow = @shadow_normal 22 | end 23 | end 24 | 25 | def draw(viewport) 26 | @shadow.draw_rot(x - 1, y - 1, 0, object.direction) 27 | @body.draw_rot(x, y, 1, object.direction) 28 | @gun.draw_rot(x, y, 2, object.gun_angle) if @gun 29 | Utils.mark_corners(object.box) if $debug 30 | end 31 | 32 | def width 33 | @body.width 34 | end 35 | 36 | def height 37 | @body.height 38 | end 39 | 40 | private 41 | 42 | def units 43 | @@units = Gosu::TexturePacker.load_json( 44 | $window, Utils.media_path('ground_units.json'), :precise) 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /13-advanced-ai/entities/components/tank_health.rb: -------------------------------------------------------------------------------- 1 | class TankHealth < Health 2 | RESPAWN_DELAY = 5000 3 | attr_accessor :health 4 | 5 | def initialize(object, object_pool) 6 | super(object, object_pool, 100, true) 7 | end 8 | 9 | def should_respawn? 10 | if @death_time 11 | Gosu.milliseconds - @death_time > RESPAWN_DELAY 12 | end 13 | end 14 | 15 | protected 16 | 17 | def draw? 18 | true 19 | end 20 | 21 | def after_death(cause) 22 | @death_time = Gosu.milliseconds 23 | object.reset_modifiers 24 | object.input.stats.add_death 25 | kill = object != cause ? 1 : -1 26 | cause.input.stats.add_kill(kill) 27 | Thread.new do 28 | sleep(rand(0.1..0.3)) 29 | Explosion.new(@object_pool, x, y, cause) 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /13-advanced-ai/entities/components/tank_sounds.rb: -------------------------------------------------------------------------------- 1 | class TankSounds < Component 2 | def initialize(object, object_pool) 3 | super(object) 4 | @object_pool = object_pool 5 | end 6 | 7 | def update 8 | id = object.object_id 9 | if object.physics.moving? 10 | move_volume = Utils.volume( 11 | object, @object_pool.camera) 12 | pan = Utils.pan(object, @object_pool.camera) 13 | if driving_sound.paused?(id) 14 | driving_sound.resume(id) 15 | elsif driving_sound.stopped?(id) 16 | driving_sound.play(id, pan, 0.5, 1, true) 17 | end 18 | driving_sound.volume_and_pan(id, move_volume * 0.5, pan) 19 | else 20 | if driving_sound.playing?(id) 21 | driving_sound.pause(id) 22 | end 23 | end 24 | end 25 | 26 | def collide 27 | vol, pan = Utils.volume_and_pan( 28 | object, @object_pool.camera) 29 | crash_sound.play(self.object_id, pan, vol, 1, false) 30 | end 31 | 32 | private 33 | 34 | def driving_sound 35 | @@driving_sound ||= StereoSample.new( 36 | $window, Utils.media_path('tank_driving.mp3')) 37 | end 38 | 39 | def crash_sound 40 | @@crash_sound ||= StereoSample.new( 41 | $window, Utils.media_path('metal_interaction2.wav')) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /13-advanced-ai/entities/damage.rb: -------------------------------------------------------------------------------- 1 | class Damage < GameObject 2 | MAX_INSTANCES = 300 3 | @@instances = [] 4 | 5 | def initialize(object_pool, x, y) 6 | super 7 | DamageGraphics.new(self) 8 | track(self) 9 | end 10 | 11 | def effect? 12 | true 13 | end 14 | 15 | private 16 | 17 | def track(instance) 18 | if @@instances.size < MAX_INSTANCES 19 | @@instances << instance 20 | else 21 | out = @@instances.shift 22 | out.mark_for_removal 23 | @@instances << instance 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /13-advanced-ai/entities/explosion.rb: -------------------------------------------------------------------------------- 1 | class Explosion < GameObject 2 | 3 | def initialize(object_pool, x, y, source) 4 | super(object_pool, x, y) 5 | @source = source 6 | @object_pool = object_pool 7 | if @object_pool.map.can_move_to?(x, y) 8 | Damage.new(@object_pool, x, y) 9 | end 10 | ExplosionGraphics.new(self) 11 | ExplosionSounds.play(self, object_pool.camera) 12 | inflict_damage 13 | end 14 | 15 | def effect? 16 | true 17 | end 18 | 19 | def mark_for_removal 20 | super 21 | end 22 | 23 | private 24 | 25 | def inflict_damage 26 | object_pool.nearby(self, 100).each do |obj| 27 | if obj.respond_to?(:health) 28 | obj.health.inflict_damage( 29 | Math.sqrt(3 * 100 - Utils.distance_between( 30 | obj.x, obj.y, @x, @y)), @source) 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /13-advanced-ai/entities/game_object.rb: -------------------------------------------------------------------------------- 1 | class GameObject 2 | attr_reader :x, :y, :location, :components 3 | def initialize(object_pool, x, y) 4 | @x, @y = x, y 5 | @location = [x, y] 6 | @components = [] 7 | @object_pool = object_pool 8 | @object_pool.add(self) 9 | end 10 | 11 | def move(new_x, new_y) 12 | return if new_x == @x && new_y == @y 13 | @object_pool.tree_remove(self) 14 | @x = new_x 15 | @y = new_y 16 | @location = [new_x, new_y] 17 | @object_pool.tree_insert(self) 18 | end 19 | 20 | def update 21 | @components.map(&:update) 22 | end 23 | 24 | def draw(viewport) 25 | @components.each { |c| c.draw(viewport) } 26 | end 27 | 28 | def removable? 29 | @removable 30 | end 31 | 32 | def mark_for_removal 33 | @removable = true 34 | end 35 | 36 | def on_collision(object) 37 | end 38 | 39 | def effect? 40 | false 41 | end 42 | 43 | def box 44 | end 45 | 46 | def collide 47 | end 48 | 49 | protected 50 | 51 | def object_pool 52 | @object_pool 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /13-advanced-ai/entities/powerups/fire_rate_powerup.rb: -------------------------------------------------------------------------------- 1 | class FireRatePowerup < Powerup 2 | def pickup(object) 3 | if object.class == Tank 4 | if object.fire_rate_modifier < 2 5 | object.fire_rate_modifier += 0.25 6 | end 7 | true 8 | end 9 | end 10 | 11 | def graphics 12 | :straight_gun 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /13-advanced-ai/entities/powerups/health_powerup.rb: -------------------------------------------------------------------------------- 1 | class HealthPowerup < Powerup 2 | def pickup(object) 3 | if object.class == Tank 4 | object.health.increase(25) 5 | true 6 | end 7 | end 8 | 9 | def graphics 10 | :life_up 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /13-advanced-ai/entities/powerups/powerup.rb: -------------------------------------------------------------------------------- 1 | class Powerup < GameObject 2 | def initialize(object_pool, x, y) 3 | super 4 | PowerupGraphics.new(self, graphics) 5 | end 6 | 7 | def box 8 | [x - 8, y - 8, 9 | x + 8, y - 8, 10 | x + 8, y + 8, 11 | x - 8, y + 8] 12 | end 13 | 14 | def on_collision(object) 15 | if pickup(object) 16 | PowerupSounds.play(object, object_pool.camera) 17 | remove 18 | end 19 | end 20 | 21 | def pickup(object) 22 | # override and implement application 23 | end 24 | 25 | def remove 26 | object_pool.powerup_respawn_queue.enqueue( 27 | respawn_delay, 28 | self.class, x, y) 29 | mark_for_removal 30 | end 31 | 32 | def respawn_delay 33 | 30 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /13-advanced-ai/entities/powerups/powerup_respawn_queue.rb: -------------------------------------------------------------------------------- 1 | class PowerupRespawnQueue 2 | RESPAWN_DELAY = 1000 3 | def initialize 4 | @respawn_queue = {} 5 | @last_respawn = Gosu.milliseconds 6 | end 7 | 8 | def enqueue(delay_seconds, type, x, y) 9 | respawn_at = Gosu.milliseconds + delay_seconds * 1000 10 | @respawn_queue[respawn_at.to_i] = [type, x, y] 11 | end 12 | 13 | def respawn(object_pool) 14 | now = Gosu.milliseconds 15 | return if now - @last_respawn < RESPAWN_DELAY 16 | @respawn_queue.keys.each do |k| 17 | next if k > now # not yet 18 | type, x, y = @respawn_queue.delete(k) 19 | type.new(object_pool, x, y) 20 | end 21 | @last_respawn = now 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /13-advanced-ai/entities/powerups/repair_powerup.rb: -------------------------------------------------------------------------------- 1 | class RepairPowerup < Powerup 2 | def pickup(object) 3 | if object.class == Tank 4 | if object.health.health < 100 5 | object.health.restore 6 | end 7 | true 8 | end 9 | end 10 | 11 | def graphics 12 | :repair 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /13-advanced-ai/entities/powerups/tank_speed_powerup.rb: -------------------------------------------------------------------------------- 1 | class TankSpeedPowerup < Powerup 2 | def pickup(object) 3 | if object.class == Tank 4 | if object.speed_modifier < 1.5 5 | object.speed_modifier += 0.10 6 | end 7 | true 8 | end 9 | end 10 | 11 | def graphics 12 | :wingman 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /13-advanced-ai/entities/score_display.rb: -------------------------------------------------------------------------------- 1 | class ScoreDisplay 2 | def initialize(object_pool, font_size=30) 3 | @font_size = font_size 4 | tanks = object_pool.objects.select do |o| 5 | o.class == Tank 6 | end 7 | stats = tanks.map(&:input).map(&:stats) 8 | stats.sort! do |stat1, stat2| 9 | stat2.kills <=> stat1.kills 10 | end 11 | create_stats_image(stats) 12 | end 13 | 14 | def create_stats_image(stats) 15 | text = stats.map do |stat| 16 | "#{stat.kills}: #{stat.name} " 17 | end.join("\n") 18 | @stats_image = Gosu::Image.from_text( 19 | $window, text, Utils.main_font, @font_size) 20 | end 21 | 22 | def draw 23 | @stats_image.draw( 24 | $window.width / 2 - @stats_image.width / 2, 25 | $window.height / 4 + 30, 26 | 1000) 27 | end 28 | 29 | def draw_top_right 30 | @stats_image.draw( 31 | $window.width - @stats_image.width - 20, 32 | 20, 33 | 1000) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /13-advanced-ai/entities/tree.rb: -------------------------------------------------------------------------------- 1 | class Tree < GameObject 2 | attr_reader :health, :graphics 3 | 4 | def initialize(object_pool, x, y, seed) 5 | super(object_pool, x, y) 6 | @graphics = TreeGraphics.new(self, seed) 7 | @health = Health.new(self, object_pool, 30, false) 8 | @angle = rand(-15..15) 9 | end 10 | 11 | def on_collision(object) 12 | @graphics.shake(object.direction) 13 | end 14 | 15 | def box 16 | [@x, @y] 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /13-advanced-ai/game_states/demo_state.rb: -------------------------------------------------------------------------------- 1 | class DemoState < PlayState 2 | attr_accessor :tank 3 | 4 | def enter 5 | # Prevent reactivating HUD 6 | end 7 | 8 | def update 9 | super 10 | @score_display = ScoreDisplay.new( 11 | object_pool, 20) 12 | end 13 | 14 | def draw 15 | super 16 | @score_display.draw_top_right 17 | end 18 | 19 | def button_down(id) 20 | super 21 | if id == Gosu::KbSpace 22 | target_tank = @tanks.reject do |t| 23 | t == @camera.target 24 | end.sample 25 | switch_to_tank(target_tank) 26 | end 27 | end 28 | 29 | private 30 | 31 | def create_tanks(amount) 32 | @map.spawn_points(amount * 3) 33 | @tanks = [] 34 | amount.times do |i| 35 | @tanks << Tank.new(@object_pool, AiInput.new( 36 | @names.random, @object_pool)) 37 | end 38 | target_tank = @tanks.sample 39 | @hud = HUD.new(@object_pool, target_tank) 40 | @hud.active = false 41 | switch_to_tank(target_tank) 42 | end 43 | 44 | def switch_to_tank(tank) 45 | @camera.target = tank 46 | @hud.player = tank 47 | self.tank = tank 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /13-advanced-ai/game_states/game_state.rb: -------------------------------------------------------------------------------- 1 | class GameState 2 | 3 | def self.switch(new_state) 4 | $window.state && $window.state.leave 5 | $window.state = new_state 6 | new_state.enter 7 | end 8 | 9 | def enter 10 | end 11 | 12 | def leave 13 | end 14 | 15 | def draw 16 | end 17 | 18 | def update 19 | end 20 | 21 | def needs_redraw? 22 | true 23 | end 24 | 25 | def button_down(id) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /13-advanced-ai/main.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'gosu' 4 | require 'gosu_texture_packer' 5 | require 'perlin_noise' 6 | 7 | root_dir = File.dirname(__FILE__) 8 | require_pattern = File.join(root_dir, '**/*.rb') 9 | @failed = [] 10 | 11 | # Dynamically require everything 12 | Dir.glob(require_pattern).each do |f| 13 | next if f.end_with?('_spec.rb') 14 | next if f.end_with?('/main.rb') 15 | begin 16 | require_relative f.gsub("#{root_dir}/", '') 17 | rescue 18 | # May fail if parent class not required yet 19 | @failed << f 20 | end 21 | end 22 | 23 | # Retry unresolved requires 24 | @failed.each do |f| 25 | require_relative f.gsub("#{root_dir}/", '') 26 | end 27 | 28 | $debug = false 29 | $window = GameWindow.new 30 | GameState.switch(MenuState.instance) 31 | $window.show 32 | -------------------------------------------------------------------------------- /13-advanced-ai/misc/axis_aligned_bounding_box.rb: -------------------------------------------------------------------------------- 1 | class AxisAlignedBoundingBox 2 | attr_reader :center, :half_dimension 3 | def initialize(center, half_dimension) 4 | @center = center 5 | @half_dimension = half_dimension 6 | @dhx = (@half_dimension[0] - @center[0]).abs 7 | @dhy = (@half_dimension[1] - @center[1]).abs 8 | end 9 | 10 | def contains?(point) 11 | return false unless (@center[0] + @dhx) >= point[0] 12 | return false unless (@center[0] - @dhx) <= point[0] 13 | return false unless (@center[1] + @dhy) >= point[1] 14 | return false unless (@center[1] - @dhy) <= point[1] 15 | true 16 | end 17 | 18 | def intersects?(other) 19 | ocx, ocy = other.center 20 | ohx, ohy = other.half_dimension 21 | odhx = (ohx - ocx).abs 22 | return false unless (@center[0] + @dhx) >= (ocx - odhx) 23 | return false unless (@center[0] - @dhx) <= (ocx + odhx) 24 | odhy = (ohy - ocy).abs 25 | return false unless (@center[1] + @dhy) >= (ocy - odhy) 26 | return false unless (@center[1] - @dhy) <= (ocy + odhy) 27 | true 28 | end 29 | 30 | def to_s 31 | "c: #{@center}, h: #{@half_dimension}" 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /13-advanced-ai/misc/game_window.rb: -------------------------------------------------------------------------------- 1 | class GameWindow < Gosu::Window 2 | attr_accessor :state 3 | 4 | def initialize 5 | super((ENV['w'] || 800).to_i, 6 | (ENV['h'] || 600).to_i, 7 | (ENV['fs'] ? true : false)) 8 | end 9 | 10 | def update 11 | Utils.track_update_interval 12 | @state.update 13 | end 14 | 15 | def draw 16 | @state.draw 17 | end 18 | 19 | def needs_redraw? 20 | @state.needs_redraw? 21 | end 22 | 23 | def needs_cursor? 24 | Utils.update_interval > 200 25 | end 26 | 27 | def button_down(id) 28 | @state.button_down(id) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /13-advanced-ai/misc/names.rb: -------------------------------------------------------------------------------- 1 | class Names 2 | def initialize(file) 3 | @names = File.read(file).split("\n").reject do |n| 4 | n.size > 12 5 | end 6 | end 7 | 8 | def random 9 | name = @names.sample 10 | @names.delete(name) 11 | name 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /13-advanced-ai/misc/stats.rb: -------------------------------------------------------------------------------- 1 | class Stats 2 | attr_reader :name, :kills, :deaths, :shots, :changed_at 3 | def initialize(name) 4 | @name = name 5 | @kills = @deaths = @shots = @damage = @damage_dealt = 0 6 | changed 7 | end 8 | 9 | def add_kill(amount = 1) 10 | @kills += amount 11 | changed 12 | end 13 | 14 | def add_death 15 | @deaths += 1 16 | changed 17 | end 18 | 19 | def add_shot 20 | @shots += 1 21 | changed 22 | end 23 | 24 | def add_damage(amount) 25 | @damage += amount 26 | changed 27 | end 28 | 29 | def damage 30 | @damage.round 31 | end 32 | 33 | def add_damage_dealt(amount) 34 | @damage_dealt += amount 35 | changed 36 | end 37 | 38 | def damage_dealt 39 | @damage_dealt.round 40 | end 41 | 42 | def to_s 43 | "[kills: #{@kills}, " \ 44 | "deaths: #{@deaths}, " \ 45 | "shots: #{@shots}, " \ 46 | "damage: #{damage}, " \ 47 | "damage_dealt: #{damage_dealt}]" 48 | end 49 | 50 | private 51 | 52 | def changed 53 | @changed_at = Gosu.milliseconds 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /ATTRIBUTION.md: -------------------------------------------------------------------------------- 1 | # Attribution 2 | 3 | ## Graphics 4 | 5 | - http://opengameart.org/content/explosion-sheet 6 | - http://opengameart.org/content/country-field 7 | - http://opengameart.org/content/trees-and-bushes 8 | - http://opengameart.org/content/tanks-and-trucks 9 | - http://opengameart.org/content/high-resolution-crosshairs 10 | - http://opengameart.org/content/boxes-and-barrels 11 | - http://opengameart.org/content/details-for-damaged-and-dirty-textures 12 | - http://opengameart.org/content/pickups-powerups 13 | 14 | ## Audio 15 | 16 | - http://opengameart.org/content/menu-music-loop 17 | - http://opengameart.org/content/big-explosion 18 | - http://opengameart.org/content/action-shooter-soundset-wwvi 19 | - http://soundbible.com/1325-Tank.html 20 | - http://opengameart.org/content/crash-collision 21 | - http://opengameart.org/content/metal-interactions 22 | - http://opengameart.org/content/sci-fi-shwop-1 23 | - http://opengameart.org/content/life-pickup-yo-frankie 24 | 25 | ## Fonts 26 | 27 | - http://www.fontsquirrel.com/fonts/Top-Secret 28 | - http://www.fontsquirrel.com/fonts/Armalite-Rifle 29 | 30 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'rake' 4 | gem 'gosu' 5 | gem 'gosu_texture_packer' 6 | gem 'gosu_tiled' 7 | gem 'perlin_noise' 8 | gem 'ruby-prof' 9 | 10 | gem 'rspec', '~> 3.0.0' 11 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | diff-lcs (1.2.5) 5 | gosu (0.8.5) 6 | gosu_texture_packer (0.1.4) 7 | gosu 8 | json 9 | rmagick 10 | gosu_tiled (0.1.1) 11 | gosu 12 | json 13 | json (1.8.1) 14 | perlin_noise (0.1.2) 15 | rake (10.3.2) 16 | rmagick (2.13.3) 17 | rspec (3.0.0) 18 | rspec-core (~> 3.0.0) 19 | rspec-expectations (~> 3.0.0) 20 | rspec-mocks (~> 3.0.0) 21 | rspec-core (3.0.4) 22 | rspec-support (~> 3.0.0) 23 | rspec-expectations (3.0.4) 24 | diff-lcs (>= 1.2.0, < 2.0) 25 | rspec-support (~> 3.0.0) 26 | rspec-mocks (3.0.4) 27 | rspec-support (~> 3.0.0) 28 | rspec-support (3.0.4) 29 | ruby-prof (0.15.2) 30 | 31 | PLATFORMS 32 | ruby 33 | 34 | DEPENDENCIES 35 | gosu 36 | gosu_texture_packer 37 | gosu_tiled 38 | perlin_noise 39 | rake 40 | rspec (~> 3.0.0) 41 | ruby-prof 42 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Tomas Varaneckas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Source code examples from "Developing Games With Ruby" book 2 | 3 | [Get the book](https://leanpub.com/developing-games-with-ruby/) 4 | 5 | ## Are you a game developer, or want to be one? 6 | 7 | I am building a community for indie game developers, and you are welcome to join! 8 | 9 | [Game Hero - Indie Game Developer Community](https://www.gamehero.org) 10 | -------------------------------------------------------------------------------- /media/armalite_rifle.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spajus/ruby-gamedev-book-examples/f3eded21361a5672edd5072f46c1c999c233144e/media/armalite_rifle.ttf -------------------------------------------------------------------------------- /media/boxes_barrels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spajus/ruby-gamedev-book-examples/f3eded21361a5672edd5072f46c1c999c233144e/media/boxes_barrels.png -------------------------------------------------------------------------------- /media/bullet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spajus/ruby-gamedev-book-examples/f3eded21361a5672edd5072f46c1c999c233144e/media/bullet.png -------------------------------------------------------------------------------- /media/c_dot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spajus/ruby-gamedev-book-examples/f3eded21361a5672edd5072f46c1c999c233144e/media/c_dot.png -------------------------------------------------------------------------------- /media/country_field.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spajus/ruby-gamedev-book-examples/f3eded21361a5672edd5072f46c1c999c233144e/media/country_field.png -------------------------------------------------------------------------------- /media/crash.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spajus/ruby-gamedev-book-examples/f3eded21361a5672edd5072f46c1c999c233144e/media/crash.ogg -------------------------------------------------------------------------------- /media/damage1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spajus/ruby-gamedev-book-examples/f3eded21361a5672edd5072f46c1c999c233144e/media/damage1.png -------------------------------------------------------------------------------- /media/damage2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spajus/ruby-gamedev-book-examples/f3eded21361a5672edd5072f46c1c999c233144e/media/damage2.png -------------------------------------------------------------------------------- /media/damage3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spajus/ruby-gamedev-book-examples/f3eded21361a5672edd5072f46c1c999c233144e/media/damage3.png -------------------------------------------------------------------------------- /media/damage4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spajus/ruby-gamedev-book-examples/f3eded21361a5672edd5072f46c1c999c233144e/media/damage4.png -------------------------------------------------------------------------------- /media/decor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spajus/ruby-gamedev-book-examples/f3eded21361a5672edd5072f46c1c999c233144e/media/decor.png -------------------------------------------------------------------------------- /media/decor.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spajus/ruby-gamedev-book-examples/f3eded21361a5672edd5072f46c1c999c233144e/media/decor.psd -------------------------------------------------------------------------------- /media/explosion.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spajus/ruby-gamedev-book-examples/f3eded21361a5672edd5072f46c1c999c233144e/media/explosion.mp3 -------------------------------------------------------------------------------- /media/explosion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spajus/ruby-gamedev-book-examples/f3eded21361a5672edd5072f46c1c999c233144e/media/explosion.png -------------------------------------------------------------------------------- /media/fire.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spajus/ruby-gamedev-book-examples/f3eded21361a5672edd5072f46c1c999c233144e/media/fire.mp3 -------------------------------------------------------------------------------- /media/ground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spajus/ruby-gamedev-book-examples/f3eded21361a5672edd5072f46c1c999c233144e/media/ground.png -------------------------------------------------------------------------------- /media/ground_units.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spajus/ruby-gamedev-book-examples/f3eded21361a5672edd5072f46c1c999c233144e/media/ground_units.png -------------------------------------------------------------------------------- /media/menu_music.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spajus/ruby-gamedev-book-examples/f3eded21361a5672edd5072f46c1c999c233144e/media/menu_music.mp3 -------------------------------------------------------------------------------- /media/metal_interaction2.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spajus/ruby-gamedev-book-examples/f3eded21361a5672edd5072f46c1c999c233144e/media/metal_interaction2.wav -------------------------------------------------------------------------------- /media/pickups.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spajus/ruby-gamedev-book-examples/f3eded21361a5672edd5072f46c1c999c233144e/media/pickups.png -------------------------------------------------------------------------------- /media/powerup.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spajus/ruby-gamedev-book-examples/f3eded21361a5672edd5072f46c1c999c233144e/media/powerup.mp3 -------------------------------------------------------------------------------- /media/respawn.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spajus/ruby-gamedev-book-examples/f3eded21361a5672edd5072f46c1c999c233144e/media/respawn.wav -------------------------------------------------------------------------------- /media/tank_driving.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spajus/ruby-gamedev-book-examples/f3eded21361a5672edd5072f46c1c999c233144e/media/tank_driving.mp3 -------------------------------------------------------------------------------- /media/top_secret.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spajus/ruby-gamedev-book-examples/f3eded21361a5672edd5072f46c1c999c233144e/media/top_secret.ttf -------------------------------------------------------------------------------- /media/trees.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spajus/ruby-gamedev-book-examples/f3eded21361a5672edd5072f46c1c999c233144e/media/trees.png -------------------------------------------------------------------------------- /media/trees_packed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spajus/ruby-gamedev-book-examples/f3eded21361a5672edd5072f46c1c999c233144e/media/trees_packed.png -------------------------------------------------------------------------------- /media/water.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spajus/ruby-gamedev-book-examples/f3eded21361a5672edd5072f46c1c999c233144e/media/water.png --------------------------------------------------------------------------------