diff --git a/.gitignore b/.gitignore index e43b0f9..5b2294c 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ .DS_Store +_build + diff --git a/README.md b/README.md new file mode 100644 index 0000000..d74134c --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +Sector Five II +============== + +Extended edition of the game, based on a Tutorial from Learn Game Programming +with Ruby by Mark Sobkowicz. + +You can read the [credits](credits.txt) seperately. + +Installation +============ + +The are two ways to run the game. One is setting up Ruby in your environment, +along all the necessary libraries. Download the source code and run the game. + +The other is to go to this repository's releases page, and download a bundled +version. For now, I only provide a macOS bundle, but I can soon provide a +Windows one as well. + +Running or developing on macOS +============================== + +1. Install the latest Ruby version +2. brew install sdl2 libogg libvorbis​ +3. gem install gosu +4. gem install chipmunk + +Running or developing on Windows +================================ + +[...Help Needed...] +Need someone with Windows to contribute this bit. They need to be able to run +this game on their system. + +Credits +======= +The starter base code comes from a tutorial from Learn Game Programming +with Ruby by [Mark Sobkowicz](https://twitter.com/MarkSobkowicz). + +You can read the rest of the [credits](credits.txt) seperately. It includes the +attributions for the sounds and images used in this game. + +Contributing +============ + +Please follow the [CONTRIBUTING](CONTRIBUTING) guide. + +Contributors +============ + +- [petros](https://twitter.com/amiridis) ([petros.blog/games](https://petros.blog/games)) diff --git a/bullet.rb b/bullet.rb index 17a3600..31eaca5 100644 --- a/bullet.rb +++ b/bullet.rb @@ -1,7 +1,8 @@ -class Bullet +require_relative 'sprite' + +class Bullet < Sprite SPEED = 5 - attr_reader :x, :y, :radius - + def initialize(window, x, y, angle) @x = x @y = y @@ -10,16 +11,16 @@ def initialize(window, x, y, angle) @radius = 3 @window = window end - + def move @x += Gosu.offset_x(@direction, SPEED) @y += Gosu.offset_y(@direction, SPEED) end - + def draw - @image.draw(@x - @radius, @y - @radius, 1) + @image.draw(@x - @radius, @y - @radius, 1) end - + def onscreen? right = @window.width + @radius left = -@radius diff --git a/credit.rb b/credit.rb index 5432311..4b808e8 100644 --- a/credit.rb +++ b/credit.rb @@ -1,22 +1,22 @@ class Credit SPEED = 1 attr_reader :y - + def initialize(window, text, x, y) @x = x @y = @initial_y = y @text = text @font = Gosu::Font.new(24) end - + def move @y -= SPEED end - + def draw @font.draw_text(@text, @x, @y, 1) end - + def reset @y = @initial_y end diff --git a/end_scenes.rb b/end_scenes.rb new file mode 100644 index 0000000..6316e29 --- /dev/null +++ b/end_scenes.rb @@ -0,0 +1,78 @@ +require 'gosu' +require_relative 'scene' +require_relative 'credit' + +class EndScene < Scene + def initialize(enemies_destroyed) + @enemies_destroyed = enemies_destroyed + @bottom_message = "Press P to play again, or Q to quit." + @message_font = Gosu::Font.new(28) + @credits = [] + y = 700 + File.open('credits.txt').each do |line| + @credits.push(Credit.new(self, line.chomp, 100, y)) + y += 30 + end + @scene = :end + @end_music = Gosu::Song.new('sounds/from_here.ogg') + @end_music.play(true) + end + + def button_down(id) + if id == Gosu::KbP + Game.current_scene = FirstWaveScene.new + elsif id == Gosu::KbQ + Game.window.close + end + end + + def update + @credits.each do |credit| + credit.move + end + if @credits.last.y < 150 + @credits.each do |credit| + credit.reset + end + end + end + + def draw + Gosu.clip_to(50, 140, 700, 360) do + @credits.each do |credit| + credit.draw + end + end + Gosu.draw_line(0, 140, Gosu::Color::RED, Game::WINDOW_WIDTH, 140, Gosu::Color::RED) + @message_font.draw_text(@message, 40, 40, 1, 1, 1, Gosu::Color::FUCHSIA) + @message_font.draw_text(@message2, 40, 75, 1, 1, 1, Gosu::Color::FUCHSIA) + Gosu.draw_line(0, 500, Gosu::Color::RED, Game::WINDOW_WIDTH, 500, Gosu::Color::RED) + @message_font.draw_text(@bottom_message, 180, 540, 1, 1, 1, Gosu::Color::AQUA) + end +end + +class EndCountReachedScene < EndScene + def initialize(enemies_destroyed) + super(enemies_destroyed) + @message = "You made it! You destroyed #{enemies_destroyed} ships" + @message2 = "and #{Game::MAX_ENEMIES - enemies_destroyed} reached the base." + end +end + +class EndHitByEnemyScene < EndScene + def initialize(enemies_destroyed) + super(enemies_destroyed) + @message = "You were struck by an enemy ship." + @message2 = "Before your ship was destroyed, " + @message2 += "you took out #{enemies_destroyed} enemy ships." + end +end + +class EndOffTopScene < EndScene + def initialize(enemies_destroyed) + super(enemies_destroyed) + @message = "You got too close to the enemy mother ship." + @message2 = "Before your ship was destroyed, " + @message2 += "you took out #{enemies_destroyed} enemy ships." + end +end diff --git a/enemy.rb b/enemy.rb index 84e7351..cb5a6a4 100644 --- a/enemy.rb +++ b/enemy.rb @@ -1,6 +1,6 @@ -class Enemy - attr_reader :x, :y, :radius - +require_relative 'sprite' + +class Enemy < Sprite def initialize(window) @radius = 20 @x = rand(window.width - 2 * @radius) + @radius @@ -8,11 +8,11 @@ def initialize(window) @image = Gosu::Image.new('images/enemy.png') @speed = rand(1..6) end - + def move @y += @speed end - + def draw @image.draw(@x - @radius, @y - @radius, 1) end diff --git a/explosion.rb b/explosion.rb index f98b6e8..d0ca225 100644 --- a/explosion.rb +++ b/explosion.rb @@ -1,6 +1,8 @@ -class Explosion - attr_reader :x, :y, :radius, :finished - +require_relative 'sprite' + +class Explosion < Sprite + attr_reader :finished + def initialize(window, x, y) @x = x @y = y @@ -9,7 +11,7 @@ def initialize(window, x, y) @image_index = 0 @finished = false end - + def draw if @image_index < @images.count @images[@image_index].draw(@x - @radius, @y - @radius, 2) diff --git a/first_wave_scene.rb b/first_wave_scene.rb new file mode 100644 index 0000000..13ba9c0 --- /dev/null +++ b/first_wave_scene.rb @@ -0,0 +1,145 @@ +require 'gosu' +require_relative 'game' +require_relative 'scene' +require_relative 'player' +require_relative 'enemy' +require_relative 'bullet' +require_relative 'explosion' +require_relative 'end_scenes' + +class FirstWaveScene < Scene + def initialize + @scene = :first_wave + @player = Player.new(Game.window) + @enemies = [] + @bullets = [] + @explosions = [] + @enemies_appeared = 0 + @enemies_appeared_font = Gosu::Font.new(18, bold: true) + @enemies_destroyed = 0 + @enemies_destroyed_font = Gosu::Font.new(18, bold: true) + @enemies_escaped_font = Gosu::Font.new(18, bold: true) + @game_music = Gosu::Song.new('sounds/cephalopod.ogg') + @game_music.play(true) + @explosion_sound = Gosu::Sample.new('sounds/explosion.ogg') + @shooting_sound = Gosu::Sample.new('sounds/shoot.ogg') + @teleport_sound = Gosu::Sample.new('sounds/teleport.wav') + end + + def button_down(id) + if id == Gosu::KbSpace + @bullets.push Bullet.new(Game.window, @player.x, @player.y, @player.angle) + @shooting_sound.play(0.3) + end + end + + def update + @player.turn_left if Gosu.button_down?(Gosu::KbLeft) + @player.turn_right if Gosu.button_down?(Gosu::KbRight) + @player.accelerate if Gosu.button_down?(Gosu::KbUp) + @player.move + if rand < Game::ENEMY_FREQUENCY + @enemies.push Enemy.new(Game.window) + @enemies_appeared += 1 + end + @enemies.each do |enemy| + enemy.move + end + @bullets.each do |bullet| + bullet.move + end + # Detect if a bullet hits an enemy, and push an explosion. + @enemies.dup.each do |enemy| + @bullets.dup.each do |bullet| + if bullet.collides_with?(enemy) + @enemies.delete enemy + @enemies_destroyed += 1 + @bullets.delete bullet + @explosions.push Explosion.new(Game.window, enemy.x, enemy.y) + @explosion_sound.play + end + end + end + # Chain explosions. If an enemy is near an ongoing explosion, it explodes + # too. + @enemies.dup.each do |enemy| + @explosions.dup.each do |explosion| + if enemy.collides_with?(explosion) + @enemies.delete enemy + @enemies_destroyed += 1 + @explosions.push Explosion.new(Game.window, enemy.x, enemy.y) + @explosion_sound.play + end + end + end + # Remove explosions that have finished animating. + @explosions.dup.each do |explosion| + @explosions.delete explosion if explosion.finished + end + # Remove enemies that have moved beyond the bottom of the screen. + @enemies.dup.each do |enemy| + if enemy.y > Game::WINDOW_HEIGHT + enemy.radius + @enemies.delete enemy + @teleport_sound.play(0.3) + end + end + # Remove bullets that have moved out of the screen boundaries. + @bullets.dup.each do |bullet| + @bullets.delete bullet unless bullet.onscreen? + end + if @enemies_appeared > Game::MAX_ENEMIES + Game.current_scene = EndCountReachedScene.new(@enemies_destroyed) + end + @enemies.each do |enemy| + #distance = Gosu.distance(enemy.x, enemy.y, @player.x, @player.y) + #if distance < @player.radius + enemy.radius + if enemy.collides_with?(@player) + Game.current_scene = EndHitByEnemyScene.new(@enemies_destroyed) + end + end + if @player.y < @player.radius + Game.current_scene = EndOffTopScene.new(@enemies_destroyed) + end + end + + def draw + @player.draw + @enemies.each do |enemy| + enemy.draw + end + @bullets.each do |bullet| + bullet.draw + end + @explosions.each do |explosion| + explosion.draw + end + @enemies_appeared_font.draw_text( + "Fleet: #{Game::MAX_ENEMIES - @enemies_appeared}", + 460, + 10, + 1, + 1, + 1, + Gosu::Color::WHITE + ) + @enemies_destroyed_font.draw_text( + "Destroyed: #{@enemies_destroyed}", + 540, + 10, + 1, + 1, + 1, + Gosu::Color::GREEN + ) + @enemies_escaped_font.draw_text( + "Escaped: #{Game::MAX_ENEMIES - @enemies_destroyed}", + 660, + 10, + 1, + 1, + 1, + Gosu::Color::RED + ) + end + +end \ No newline at end of file diff --git a/game.rb b/game.rb new file mode 100644 index 0000000..85507ff --- /dev/null +++ b/game.rb @@ -0,0 +1,17 @@ +require_relative 'start_scene' + +class Game + WINDOW_WIDTH = 800 + WINDOW_HEIGHT = 600 + ENEMY_FREQUENCY = 0.05 + MAX_ENEMIES = 100 + + class << self + attr_accessor :current_scene, :window + end + + def initialize(window) + Game.current_scene = StartScene.new + Game.window = window + end +end diff --git a/main.rb b/main.rb index 9dc6494..703615d 100644 --- a/main.rb +++ b/main.rb @@ -1,250 +1,27 @@ +################################################################################ +# +# Sector Five II +# require 'gosu' -require_relative 'player' -require_relative 'enemy' -require_relative 'bullet' -require_relative 'explosion' -require_relative 'credit' +require_relative 'game' class SectorFive < Gosu::Window - WIDTH = 800 - HEIGHT = 600 - ENEMY_FREQUENCY = 0.05 - MAX_ENEMIES = 100 - def initialize - super(WIDTH, HEIGHT) + super(Game::WINDOW_WIDTH, Game::WINDOW_HEIGHT) self.caption = 'Sector Five II' - @background_image = Gosu::Image.new('images/start_screen.png') - @scene = :start - @start_music = Gosu::Song.new('sounds/Lost Frontier.ogg') - @start_music.play(true) + Game.new(self) end - + def button_down(id) - case @scene - when :start - button_down_start(id) - when :game - button_down_game(id) - when :end - button_down_end(id) - end - end - - def button_down_start(id) - initialize_game - end - - def button_down_game(id) - if id == Gosu::KbSpace - @bullets.push Bullet.new(self, @player.x, @player.y, @player.angle) - @shooting_sound.play(0.3) - end - end - - def button_down_end(id) - if id == Gosu::KbP - initialize_game - elsif id == Gosu::KbQ - close - end + Game.current_scene.button_down(id) end - - def initialize_game - @player = Player.new(self) - @enemies = [] - @bullets = [] - @explosions = [] - @enemies_appeared = 0 - @enemies_appeared_font = Gosu::Font.new(18, bold: true) - @enemies_destroyed = 0 - @enemies_destroyed_font = Gosu::Font.new(18, bold: true) - @enemies_escaped_font = Gosu::Font.new(18, bold: true) - @scene = :game - @game_music = Gosu::Song.new('sounds/Cephalopod.ogg') - @game_music.play(true) - @explosion_sound = Gosu::Sample.new('sounds/explosion.ogg') - @shooting_sound = Gosu::Sample.new('sounds/shoot.ogg') - @teleport_sound = Gosu::Sample.new('sounds/teleport.wav') - end - - def initialize_end(fate) - case fate - when :count_reached - @message = "You made it! You destroyed #{@enemies_destroyed} ships" - @message2 = "and #{100-@enemies_destroyed} reached the base." - when :hit_by_enemy - @message = "You were struck by an enemy ship." - @message2 = "Before your ship was destroyed, " - @message2 += "you took out #{@enemies_destroyed} enemy ships." - when :off_top - @message = "You got too close to the enemy mother ship." - @message2 = "Before your ship was destroyed, " - @message2 += "you took out #{@enemies_destroyed} enemy ships." - end - @bottom_message = "Press P to play again, or Q to quit." - @message_font = Gosu::Font.new(28) - @credits = [] - y = 700 - File.open('credits.txt').each do |line| - @credits.push(Credit.new(self, line.chomp, 100, y)) - y += 30 - end - @scene = :end - @end_music = Gosu::Song.new('sounds/FromHere.ogg') - @end_music.play(true) - end - + def update - case @scene - when :game - update_game - when :end - update_end - end + Game.current_scene.update end - - def update_game - @player.turn_left if button_down?(Gosu::KbLeft) - @player.turn_right if button_down?(Gosu::KbRight) - @player.accelerate if button_down?(Gosu::KbUp) - @player.move - if rand < ENEMY_FREQUENCY - @enemies.push Enemy.new(self) - @enemies_appeared += 1 - end - @enemies.each do |enemy| - enemy.move - end - @bullets.each do |bullet| - bullet.move - end - # Detect if a bullet hits an enemy, and push an explosion. - @enemies.dup.each do |enemy| - @bullets.dup.each do |bullet| - distance = Gosu.distance(enemy.x, enemy.y, bullet.x, bullet.y) - if distance < enemy.radius + bullet.radius - @enemies.delete enemy - @enemies_destroyed += 1 - @bullets.delete bullet - @explosions.push Explosion.new(self, enemy.x, enemy.y) - @explosion_sound.play - end - end - end - # Chain explosions. If an enemy is near an ongoing explosion, it explodes - # too. - @enemies.dup.each do |enemy| - @explosions.dup.each do |explosion| - distance = Gosu.distance(enemy.x, enemy.y, explosion.x, explosion.y) - if distance < enemy.radius + explosion.radius - @enemies.delete enemy - @enemies_destroyed += 1 - @explosions.push Explosion.new(self, enemy.x, enemy.y) - @explosion_sound.play - end - end - end - # Remove explosions that have finished animating. - @explosions.dup.each do |explosion| - @explosions.delete explosion if explosion.finished - end - # Remove enemies that have moved beyond the bottom of the screen. - @enemies.dup.each do |enemy| - if enemy.y > HEIGHT + enemy.radius - @enemies.delete enemy - @teleport_sound.play(0.3) - end - end - # Remove bullets that have moved out of the screen boundaries. - @bullets.dup.each do |bullet| - @bullets.delete bullet unless bullet.onscreen? - end - initialize_end(:count_reached) if @enemies_appeared > MAX_ENEMIES - @enemies.each do |enemy| - distance = Gosu.distance(enemy.x, enemy.y, @player.x, @player.y) - initialize_end(:hit_by_enemy) if distance < @player.radius + enemy.radius - end - initialize_end(:off_top) if @player.y < @player.radius - end - - def update_end - @credits.each do |credit| - credit.move - end - if @credits.last.y < 150 - @credits.each do |credit| - credit.reset - end - end - end - + def draw - case @scene - when :start - draw_start - when :game - draw_game - when :end - draw_end - end - end - - def draw_start - @background_image.draw(0,0,0) - end - - def draw_game - @player.draw - @enemies.each do |enemy| - enemy.draw - end - @bullets.each do |bullet| - bullet.draw - end - @explosions.each do |explosion| - explosion.draw - end - @enemies_appeared_font.draw_text( - "Fleet: #{MAX_ENEMIES - @enemies_appeared}", - 460, - 10, - 1, - 1, - 1, - Gosu::Color::WHITE - ) - @enemies_destroyed_font.draw_text( - "Destroyed: #{@enemies_destroyed}", - 560, - 10, - 1, - 1, - 1, - Gosu::Color::GREEN - ) - @enemies_escaped_font.draw_text( - "Escaped: #{MAX_ENEMIES - @enemies_destroyed}", - 660, - 10, - 1, - 1, - 1, - Gosu::Color::RED - ) - end - - def draw_end - clip_to(50, 140, 700, 360) do - @credits.each do |credit| - credit.draw - end - end - draw_line(0, 140, Gosu::Color::RED, WIDTH, 140, Gosu::Color::RED) - @message_font.draw_text(@message, 40, 40, 1, 1, 1, Gosu::Color::FUCHSIA) - @message_font.draw_text(@message2, 40, 75, 1, 1, 1, Gosu::Color::FUCHSIA) - draw_line(0, 500, Gosu::Color::RED, WIDTH, 500, Gosu::Color::RED) - @message_font.draw_text(@bottom_message, 180, 540, 1, 1, 1, Gosu::Color::AQUA) + Game.current_scene.draw end end diff --git a/player.rb b/player.rb index c26bc34..6233663 100644 --- a/player.rb +++ b/player.rb @@ -1,10 +1,12 @@ -class Player +require_relative 'sprite' + +class Player < Sprite ROTATION_SPEED = 3 ACCELERATION = 2 FRICTION = 0.9 - - attr_reader :x, :y, :angle, :radius - + + attr_reader :angle + def initialize(window) @x = 200 @y = 200 @@ -15,24 +17,24 @@ def initialize(window) @radius = 20 @window = window end - + def draw @image.draw_rot(@x, @y, 1, @angle) end - + def turn_right @angle += ROTATION_SPEED end - + def turn_left @angle -= ROTATION_SPEED end - + def accelerate @velocity_x += Gosu.offset_x(@angle, ACCELERATION) @velocity_y += Gosu.offset_y(@angle, ACCELERATION) end - + def move @x += @velocity_x @y += @velocity_y diff --git a/scene.rb b/scene.rb new file mode 100644 index 0000000..97f7159 --- /dev/null +++ b/scene.rb @@ -0,0 +1,13 @@ +class Scene + def initialize + end + + def button_down(id) + end + + def update + end + + def draw + end +end diff --git a/sprite.rb b/sprite.rb new file mode 100644 index 0000000..9fdc22c --- /dev/null +++ b/sprite.rb @@ -0,0 +1,14 @@ +require 'gosu' + +class Sprite + attr_reader :x, :y, :radius + + def initialize + end + + def collides_with?(sprite) + distance = Gosu.distance(x, y, sprite.x, sprite.y) + return distance < @radius + sprite.radius + end + +end diff --git a/start_scene.rb b/start_scene.rb new file mode 100644 index 0000000..4376e54 --- /dev/null +++ b/start_scene.rb @@ -0,0 +1,23 @@ +require 'gosu' +require_relative 'scene' +require_relative 'game' +require_relative 'first_wave_scene' + +class StartScene < Scene + def initialize + @background_image = Gosu::Image.new('images/start_screen.png') + @start_music = Gosu::Song.new('sounds/lost_frontier.ogg') + @start_music.play(true) + end + + def button_down(id) + Game.current_scene = FirstWaveScene.new + super + end + + def draw + @background_image.draw(0,0,0) + super + end + +end \ No newline at end of file