# Encoding: UTF-8 # A simple jump-and-run/platformer game with a tile-based map. # Shows how to # * implement jumping/gravity # * implement scrolling using Window#translate # * implement a simple tile-based map # * load levels from primitive text files # Some exercises, starting at the real basics: # 0) understand the existing code! # As shown in the tutorial: # 1) change it use Gosu's Z-ordering # 2) add gamepad support # 3) add a score as in the tutorial game # 4) similarly, add sound effects for various events # Exploring this game's code and Gosu: # 5) make the player wider, so he doesn't fall off edges as easily # 6) add background music (check if playing in Window#update to implement # looping) # 7) implement parallax scrolling for the star background! # Getting tricky: # 8) optimize Map#draw so only tiles on screen are drawn (needs modulo, a pen # and paper to figure out) # 9) add loading of next level when all gems are collected # ...Enemies, a more sophisticated object system, weapons, title and credits # screens... require "gosu" WIDTH, HEIGHT = 640, 480 module Tiles Grass = 0 Earth = 1 end class CollectibleGem attr_reader :x, :y def initialize(image, x, y) @image = image @x, @y = x, y end def draw # Draw, slowly rotating @image.draw_rot(@x, @y, 0, 25 * Math.sin(Gosu.milliseconds / 133.7)) end end # Player class. class Player attr_reader :x, :y def initialize(map, x, y) @x, @y = x, y @dir = :left @vy = 0 # Vertical velocity @map = map # Load all animation frames @standing, @walk1, @walk2, @jump = *Gosu::Image.load_tiles("media/cptn_ruby.png", 50, 50) # This always points to the frame that is currently drawn. # This is set in update, and used in draw. @cur_image = @standing end def draw # Flip vertically when facing to the left. if @dir == :left offs_x = -25 factor = 1.0 else offs_x = 25 factor = -1.0 end @cur_image.draw(@x + offs_x, @y - 49, 0, factor, 1.0) end # Could the object be placed at x + offs_x/y + offs_y without being stuck? def would_fit(offs_x, offs_y) # Check at the center/top and center/bottom for map collisions not @map.solid?(@x + offs_x, @y + offs_y) and not @map.solid?(@x + offs_x, @y + offs_y - 45) end def update(move_x) # Select image depending on action if (move_x == 0) @cur_image = @standing else @cur_image = (Gosu.milliseconds / 175 % 2 == 0) ? @walk1 : @walk2 end if (@vy < 0) @cur_image = @jump end # Directional walking, horizontal movement if move_x > 0 @dir = :right move_x.times { if would_fit(1, 0) then @x += 1 end } end if move_x < 0 @dir = :left (-move_x).times { if would_fit(-1, 0) then @x -= 1 end } end # Acceleration/gravity # By adding 1 each frame, and (ideally) adding vy to y, the player's # jumping curve will be the parabole we want it to be. @vy += 1 # Vertical movement if @vy > 0 @vy.times { if would_fit(0, 1) then @y += 1 else @vy = 0 end } end if @vy < 0 (-@vy).times { if would_fit(0, -1) then @y -= 1 else @vy = 0 end } end end def try_to_jump if @map.solid?(@x, @y + 1) @vy = -20 end end def collect_gems(gems) # Same as in the tutorial game. gems.reject! do |c| (c.x - @x).abs < 50 and (c.y - @y).abs < 50 end end end # Map class holds and draws tiles and gems. class Map attr_reader :width, :height, :gems def initialize(filename) # Load 60x60 tiles, 5px overlap in all four directions. @tileset = Gosu::Image.load_tiles("media/tileset.png", 60, 60, tileable: true) gem_img = Gosu::Image.new("media/gem.png") @gems = [] lines = File.readlines(filename).map { |line| line.chomp } @height = lines.size @width = lines[0].size @tiles = Array.new(@width) do |x| Array.new(@height) do |y| case lines[y][x, 1] when '"' Tiles::Grass when '#' Tiles::Earth when 'x' @gems.push(CollectibleGem.new(gem_img, x * 50 + 25, y * 50 + 25)) nil else nil end end end end def draw # Very primitive drawing function: # Draws all the tiles, some off-screen, some on-screen. @height.times do |y| @width.times do |x| tile = @tiles[x][y] if tile # Draw the tile with an offset (tile images have some overlap) # Scrolling is implemented here just as in the game objects. @tileset[tile].draw(x * 50 - 5, y * 50 - 5, 0) end end end @gems.each { |c| c.draw } end # Solid at a given pixel position? def solid?(x, y) y < 0 || @tiles[x / 50][y / 50] end end class CptnRuby < (Example rescue Gosu::Window) def initialize super WIDTH, HEIGHT self.caption = "Cptn. Ruby" @sky = Gosu::Image.new("media/space.png", tileable: true) @map = Map.new("media/cptn_ruby_map.txt") @cptn = Player.new(@map, 400, 100) # The scrolling position is stored as top left corner of the screen. @camera_x = @camera_y = 0 end def update move_x = 0 move_x -= 5 if Gosu.button_down? Gosu::KB_LEFT move_x += 5 if Gosu.button_down? Gosu::KB_RIGHT @cptn.update(move_x) @cptn.collect_gems(@map.gems) # Scrolling follows player @camera_x = [[@cptn.x - WIDTH / 2, 0].max, @map.width * 50 - WIDTH].min @camera_y = [[@cptn.y - HEIGHT / 2, 0].max, @map.height * 50 - HEIGHT].min end def draw @sky.draw 0, 0, 0 Gosu.translate(-@camera_x, -@camera_y) do @map.draw @cptn.draw end end def button_down(id) case id when Gosu::KB_UP @cptn.try_to_jump when Gosu::KB_ESCAPE close else super end end end CptnRuby.new.show if __FILE__ == $0