├── screenshot.png ├── examples ├── media │ ├── gem.png │ ├── beep.wav │ ├── earth.png │ ├── smoke.png │ ├── space.png │ ├── star.png │ ├── soldier.png │ ├── tileset.png │ ├── cptn_ruby.png │ ├── explosion.wav │ ├── header@2x.psd │ ├── large_star.png │ ├── starfighter.bmp │ ├── landscape.svg │ └── cptn_ruby_map.txt ├── welcome.rb ├── tutorial.rb ├── chipmunk_and_rmagick.rb ├── text_input.rb ├── opengl_integration.rb ├── cptn_ruby.rb ├── chipmunk_integration.rb └── rmagick_integration.rb ├── .gitignore ├── gosu-examples.gemspec ├── README.md ├── LICENSE ├── lib └── gosu-examples │ ├── sidebar.rb │ └── example.rb └── bin └── gosu-examples /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gosu/gosu-examples/HEAD/screenshot.png -------------------------------------------------------------------------------- /examples/media/gem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gosu/gosu-examples/HEAD/examples/media/gem.png -------------------------------------------------------------------------------- /examples/media/beep.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gosu/gosu-examples/HEAD/examples/media/beep.wav -------------------------------------------------------------------------------- /examples/media/earth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gosu/gosu-examples/HEAD/examples/media/earth.png -------------------------------------------------------------------------------- /examples/media/smoke.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gosu/gosu-examples/HEAD/examples/media/smoke.png -------------------------------------------------------------------------------- /examples/media/space.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gosu/gosu-examples/HEAD/examples/media/space.png -------------------------------------------------------------------------------- /examples/media/star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gosu/gosu-examples/HEAD/examples/media/star.png -------------------------------------------------------------------------------- /examples/media/soldier.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gosu/gosu-examples/HEAD/examples/media/soldier.png -------------------------------------------------------------------------------- /examples/media/tileset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gosu/gosu-examples/HEAD/examples/media/tileset.png -------------------------------------------------------------------------------- /examples/media/cptn_ruby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gosu/gosu-examples/HEAD/examples/media/cptn_ruby.png -------------------------------------------------------------------------------- /examples/media/explosion.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gosu/gosu-examples/HEAD/examples/media/explosion.wav -------------------------------------------------------------------------------- /examples/media/header@2x.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gosu/gosu-examples/HEAD/examples/media/header@2x.psd -------------------------------------------------------------------------------- /examples/media/large_star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gosu/gosu-examples/HEAD/examples/media/large_star.png -------------------------------------------------------------------------------- /examples/media/starfighter.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gosu/gosu-examples/HEAD/examples/media/starfighter.bmp -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | /.config 4 | /coverage/ 5 | /InstalledFiles 6 | /pkg/ 7 | /spec/reports/ 8 | /test/tmp/ 9 | /test/version_tmp/ 10 | /tmp/ 11 | 12 | ## Specific to RubyMotion: 13 | .dat* 14 | .repl_history 15 | build/ 16 | 17 | ## Documentation cache and generated files: 18 | /.yardoc/ 19 | /_yardoc/ 20 | /doc/ 21 | /rdoc/ 22 | 23 | ## Environment normalisation: 24 | /.bundle/ 25 | /lib/bundler/man/ 26 | 27 | # for a library or gem, you might want to ignore these files since the code is 28 | # intended to run in multiple environments; otherwise, check them in: 29 | # Gemfile.lock 30 | # .ruby-version 31 | # .ruby-gemset 32 | 33 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 34 | .rvmrc 35 | -------------------------------------------------------------------------------- /gosu-examples.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = "gosu-examples" 3 | s.version = "1.0.7" 4 | s.author = "Julian Raschke" 5 | s.email = "julian@raschke.de" 6 | s.homepage = "http://www.libgosu.org/" 7 | s.summary = "Ruby examples for the Gosu library" 8 | s.description = "The `gosu-examples` tool provides an easy way to run and " + 9 | "inspect example games written for the Gosu game " + 10 | "development library." 11 | s.executables = %w(gosu-examples) 12 | s.files = %w(bin/gosu-examples LICENSE README.md) + 13 | Dir.glob("{lib,examples}/**/*.rb") + 14 | Dir.glob("{lib,examples}/media/**/*") 15 | 16 | s.add_dependency "gosu", ">= 1.0.0" 17 | end 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Gosu Examples 2 | ============= 3 | 4 | 5 | 6 | This is a collection of Ruby example games for the [Gosu library](https://www.libgosu.org/). 7 | 8 | (You can find C++ examples in the [main gosu repository](https://github.com/gosu/gosu/tree/master/examples).) 9 | 10 | To install and run the `gosu-examples` gem: 11 | 12 | ```bash 13 | gem install gosu-examples 14 | gosu-examples 15 | ``` 16 | 17 | You can also [download and unpack this repository](https://github.com/gosu/gosu-examples/archive/master.zip) and then run individual examples from the terminal: 18 | 19 | ```bash 20 | cd gosu-examples-master/examples 21 | ruby tutorial.rb 22 | ``` 23 | 24 | Some examples require the following additional libraries: 25 | 26 | ```bash 27 | gem install chipmunk 28 | gem install rmagick 29 | gem install opengl-bindings 30 | ``` 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Julian Raschke 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 all 13 | 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 THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /examples/media/landscape.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | SVG generated by Lineform 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/media/cptn_ruby_map.txt: -------------------------------------------------------------------------------- 1 | #....................................................# 2 | #....................................................# 3 | #.............xx......x.x............................# 4 | #............x..x....................................# 5 | #x....x...x..x.......#####..xxx....................x.# 6 | #.x.........................xxx.........##.........x.# 7 | #...............""..........###...##..........##.....# 8 | #..##..###..##..##...................................# 9 | #........................................xx........### 10 | #.............................###....................# 11 | ##....##.............................................# 12 | #....................##....##......##....##....##....# 13 | #.................................................x..# 14 | #...x....##....##.......x...x.....................x..# 15 | #.....x...............x...x...x...................x..# 16 | #......x...##.....##.................................# 17 | #.......x.........................................#..# 18 | #...........##........#...#...#..#.......x...........# 19 | #...#................................................# 20 | #....."""".................x.......#..#####...###....# 21 | #x....#......................##......................# 22 | #"""""#.....#.....x..................#...............# 23 | ##xxxx......#........................................# 24 | ##xxxx...#####............."...""""".................# 25 | ######"""############################################# 26 | -------------------------------------------------------------------------------- /lib/gosu-examples/sidebar.rb: -------------------------------------------------------------------------------- 1 | class Sidebar 2 | WIDTH = 300 3 | HEIGHT = 600 4 | FONT = Gosu::Font.new(20) 5 | HEADER = Gosu::Image.new("media/header@2x.psd", tileable: true) 6 | 7 | class Button 8 | HEIGHT = 25 9 | SPACING = 5 10 | TOP_Y = HEADER.height / 2 + 15 11 | 12 | attr_reader :filename 13 | 14 | def initialize(top, filename, &handler) 15 | @top, @filename, @handler = top, filename, handler 16 | end 17 | 18 | def draw(is_current) 19 | text_color = Gosu::Color::BLACK 20 | 21 | if is_current 22 | Gosu.draw_rect 0, @top, Sidebar::WIDTH, HEIGHT, 0xff_1565e5 23 | text_color = Gosu::Color::WHITE 24 | end 25 | 26 | FONT.draw_text File.basename(@filename), 13, @top + 2, 0, 1, 1, text_color 27 | end 28 | 29 | def click 30 | @handler.call 31 | end 32 | end 33 | 34 | def initialize 35 | y = Button::TOP_Y - Button::HEIGHT - Button::SPACING 36 | 37 | @buttons = Example.examples.map do |example| 38 | y += (Button::HEIGHT + Button::SPACING) 39 | 40 | Button.new(y, example.source_file) do 41 | yield(example) 42 | end 43 | end 44 | end 45 | 46 | def draw(current_filename) 47 | Gosu.draw_rect 0, 0, WIDTH, HEIGHT, Gosu::Color::WHITE 48 | HEADER.draw 0, 0, 0, 0.5, 0.5 49 | 50 | @buttons.each do |button| 51 | is_current = (button.filename == current_filename) 52 | button.draw(is_current) 53 | end 54 | end 55 | 56 | def click(x, y) 57 | index = (y - Button::TOP_Y).floor / (Button::HEIGHT + Button::SPACING) 58 | @buttons[index].click if @buttons[index] 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /examples/welcome.rb: -------------------------------------------------------------------------------- 1 | require "gosu" 2 | 3 | WIDTH, HEIGHT = 640, 480 4 | 5 | class Welcome < (Example rescue Gosu::Window) 6 | PADDING = 20 7 | 8 | def initialize 9 | super WIDTH, HEIGHT 10 | 11 | self.caption = "Welcome!" 12 | 13 | text = 14 | "Welcome to the Gosu Example Box! 15 | 16 | This little tool lets you launch any of Gosu’s example games from the list on the right hand side of the screen. 17 | 18 | Every example can be run both from this tool and from the terminal/command line as a stand-alone Ruby script. 19 | 20 | Keyboard shortcuts: 21 | 22 | • To see the source code of an example or feature demo, press E. 23 | • To open the ‘examples’ folder, press O. 24 | • To quit this tool, press Esc. 25 | • To toggle fullscreen mode, press Alt+Enter (Windows, Linux) or cmd+F (macOS). 26 | 27 | Why not take a look at the code for this example right now? Simply press E." 28 | 29 | # Remove all leading spaces so the text is left-aligned 30 | text.gsub! /^ +/, "" 31 | 32 | @text = Gosu::Image.from_markup text, 20, width: WIDTH - 2 * PADDING 33 | 34 | @background = Gosu::Image.new "media/space.png" 35 | end 36 | 37 | def draw 38 | draw_rotating_star_backgrounds 39 | 40 | @text.draw PADDING, PADDING, 0 41 | end 42 | 43 | def draw_rotating_star_backgrounds 44 | # Disregard the math in this method, it doesn't look as good as I thought it 45 | # would. =( 46 | 47 | angle = Gosu.milliseconds / 50.0 48 | scale = (Gosu.milliseconds % 1000) / 1000.0 49 | 50 | [1, 0].each do |extra_scale| 51 | @background.draw_rot WIDTH * 0.5, HEIGHT * 0.75, 0, angle, 0.5, 0.5, 52 | scale + extra_scale, scale + extra_scale 53 | end 54 | end 55 | end 56 | 57 | Welcome.new.show if __FILE__ == $0 58 | -------------------------------------------------------------------------------- /lib/gosu-examples/example.rb: -------------------------------------------------------------------------------- 1 | class Example 2 | attr_accessor :caption 3 | attr_reader :width, :height 4 | attr_writer :parent_window 5 | 6 | def initialize(width, height, *options) 7 | @width, @height = width, height 8 | end 9 | 10 | def draw 11 | end 12 | 13 | def update 14 | end 15 | 16 | def button_down(id) 17 | end 18 | 19 | def button_up(id) 20 | end 21 | 22 | def close 23 | # no-op, examples cannot close the containing window. 24 | end 25 | 26 | def mouse_x 27 | @parent_window && @parent_window.mouse_x 28 | end 29 | 30 | def mouse_y 31 | @parent_window && @parent_window.mouse_y 32 | end 33 | 34 | def text_input 35 | @parent_window && @parent_window.text_input 36 | end 37 | 38 | def text_input=(text_input) 39 | @parent_window && @parent_window.text_input = text_input 40 | end 41 | 42 | def self.current_source_file 43 | @current_source_file 44 | end 45 | 46 | def self.current_source_file=(current_source_file) 47 | @current_source_file = current_source_file 48 | end 49 | 50 | def self.inherited(subclass) 51 | @@examples ||= {} 52 | @@examples[subclass] = self.current_source_file 53 | end 54 | 55 | def self.examples 56 | @@examples.keys 57 | end 58 | 59 | def self.source_file 60 | @@examples[self] 61 | end 62 | 63 | def self.initial_example 64 | @@examples.keys.find { |cls| cls.name.end_with? "::Welcome" } 65 | end 66 | 67 | def self.load_examples(pattern) 68 | Dir.glob(pattern) do |file| 69 | begin 70 | # Remember which file we are loading. 71 | Example.current_source_file = File.expand_path(file) 72 | 73 | # Load the example in a sandbox module (second parameter to load()). This way, examples can 74 | # define classes and constants with the same names, and they will not collide. 75 | # 76 | # load() does not let us refer to the anonymous module it creates, but we can enumerate all 77 | # loaded examples using Example.examples thanks to the "inherited" callback above. 78 | load file, true 79 | rescue Exception => e 80 | puts "*** Cannot load #{file}:" 81 | puts e 82 | puts 83 | end 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /bin/gosu-examples: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "gosu" 4 | 5 | Dir.chdir "#{File.dirname __FILE__}/../examples" 6 | 7 | require_relative "../lib/gosu-examples/example" 8 | require_relative "../lib/gosu-examples/sidebar" 9 | 10 | Example.load_examples "*.rb" 11 | 12 | class ExampleBox < Gosu::Window 13 | def initialize 14 | welcome_class = Example.initial_example 15 | welcome = welcome_class.new 16 | 17 | super welcome.width + Sidebar::WIDTH, welcome.height, fullscreen: ARGV.include?("--fullscreen") 18 | 19 | @sidebar = Sidebar.new do |example_class| 20 | self.current_example = example_class.new unless @current_example.is_a? example_class 21 | end 22 | self.current_example = welcome 23 | end 24 | 25 | def update 26 | self.caption = "Gosu Example Box - #{@current_example.caption} (#{Gosu.fps} FPS)" 27 | 28 | @current_example.update 29 | end 30 | 31 | def draw 32 | @current_example.draw 33 | 34 | Gosu.flush 35 | 36 | Gosu.translate(@current_example.width, 0) do 37 | current_filename = @current_example.class.source_file 38 | @sidebar.draw(current_filename) 39 | end 40 | end 41 | 42 | def button_down(id) 43 | case id 44 | when Gosu::KB_ESCAPE 45 | close 46 | when Gosu.char_to_button_id("E") 47 | if filename = @current_example.class.source_file 48 | open_file_or_folder filename 49 | end 50 | when Gosu.char_to_button_id("O") 51 | if filename = @current_example.class.source_file 52 | open_file_or_folder File.dirname(filename) 53 | end 54 | else 55 | if id == Gosu::MS_LEFT and mouse_x >= @current_example.width 56 | @sidebar.click(mouse_x - @current_example.width, mouse_y) 57 | else 58 | @current_example.button_down(id) 59 | end 60 | end 61 | 62 | # Call super to enable alt+enter/cmd+F for toggling fullscreen mode. 63 | super 64 | end 65 | 66 | def button_up(id) 67 | @current_example.button_up(id) 68 | end 69 | 70 | def needs_cursor? 71 | true 72 | end 73 | 74 | private 75 | 76 | def current_example=(example) 77 | self.text_input = nil 78 | @current_example = example 79 | @current_example.parent_window = self 80 | self.width = @current_example.width + Sidebar::WIDTH 81 | self.height = @current_example.height 82 | end 83 | 84 | def open_file_or_folder(filename) 85 | if RUBY_PLATFORM =~ /darwin[0-9]*$/ 86 | `open '#{filename}'` 87 | elsif RUBY_PLATFORM =~ /mingw[0-9]*$/ 88 | `explorer "#{filename.gsub('/', '\\')}"` 89 | else 90 | fork { exec "xdg-open '#{filename}'" } 91 | end 92 | end 93 | end 94 | 95 | ExampleBox.new.show 96 | -------------------------------------------------------------------------------- /examples/tutorial.rb: -------------------------------------------------------------------------------- 1 | require "gosu" 2 | 3 | module ZOrder 4 | BACKGROUND, STARS, PLAYER, UI = *0..3 5 | end 6 | 7 | class Player 8 | attr_reader :score 9 | 10 | def initialize 11 | @image = Gosu::Image.new("media/starfighter.bmp") 12 | @beep = Gosu::Sample.new("media/beep.wav") 13 | @x = @y = @vel_x = @vel_y = @angle = 0.0 14 | @score = 0 15 | end 16 | 17 | def warp(x, y) 18 | @x, @y = x, y 19 | end 20 | 21 | def turn_left 22 | @angle -= 4.5 23 | end 24 | 25 | def turn_right 26 | @angle += 4.5 27 | end 28 | 29 | def accelerate 30 | @vel_x += Gosu.offset_x(@angle, 0.5) 31 | @vel_y += Gosu.offset_y(@angle, 0.5) 32 | end 33 | 34 | def move 35 | @x += @vel_x 36 | @y += @vel_y 37 | @x %= 640 38 | @y %= 480 39 | 40 | @vel_x *= 0.95 41 | @vel_y *= 0.95 42 | end 43 | 44 | def draw 45 | @image.draw_rot(@x, @y, ZOrder::PLAYER, @angle) 46 | end 47 | 48 | def collect_stars(stars) 49 | stars.reject! do |star| 50 | if Gosu.distance(@x, @y, star.x, star.y) < 35 51 | @score += 10 52 | @beep.play 53 | true 54 | else 55 | false 56 | end 57 | end 58 | end 59 | end 60 | 61 | class Star 62 | attr_reader :x, :y 63 | 64 | def initialize(animation) 65 | @animation = animation 66 | @color = Gosu::Color::BLACK.dup 67 | @color.red = rand(256 - 40) + 40 68 | @color.green = rand(256 - 40) + 40 69 | @color.blue = rand(256 - 40) + 40 70 | @x = rand * 640 71 | @y = rand * 480 72 | end 73 | 74 | def draw 75 | img = @animation[Gosu.milliseconds / 100 % @animation.size] 76 | img.draw(@x - img.width / 2.0, @y - img.height / 2.0, 77 | ZOrder::STARS, 1, 1, @color, :add) 78 | end 79 | end 80 | 81 | class Tutorial < (Example rescue Gosu::Window) 82 | def initialize 83 | super 640, 480 84 | self.caption = "Tutorial Game" 85 | 86 | @background_image = Gosu::Image.new("media/space.png", tileable: true) 87 | 88 | @player = Player.new 89 | @player.warp(320, 240) 90 | 91 | @star_anim = Gosu::Image::load_tiles("media/star.png", 25, 25) 92 | @stars = Array.new 93 | 94 | @font = Gosu::Font.new(20) 95 | end 96 | 97 | def update 98 | if Gosu.button_down? Gosu::KB_LEFT or Gosu.button_down? Gosu::GP_LEFT 99 | @player.turn_left 100 | end 101 | if Gosu.button_down? Gosu::KB_RIGHT or Gosu.button_down? Gosu::GP_RIGHT 102 | @player.turn_right 103 | end 104 | if Gosu.button_down? Gosu::KB_UP or Gosu.button_down? Gosu::GP_BUTTON_0 105 | @player.accelerate 106 | end 107 | @player.move 108 | @player.collect_stars(@stars) 109 | 110 | if rand(100) < 4 and @stars.size < 25 111 | @stars.push(Star.new(@star_anim)) 112 | end 113 | end 114 | 115 | def draw 116 | @background_image.draw(0, 0, ZOrder::BACKGROUND) 117 | @player.draw 118 | @stars.each { |star| star.draw } 119 | @font.draw_text("Score: #{@player.score}", 10, 10, ZOrder::UI, 1.0, 1.0, Gosu::Color::YELLOW) 120 | end 121 | 122 | def button_down(id) 123 | if id == Gosu::KB_ESCAPE 124 | close 125 | else 126 | super 127 | end 128 | end 129 | end 130 | 131 | Tutorial.new.show if __FILE__ == $0 132 | -------------------------------------------------------------------------------- /examples/chipmunk_and_rmagick.rb: -------------------------------------------------------------------------------- 1 | # Based on the C Demo3 demonstration distributed with Chipmunk. 2 | # Also with some help from the chipmunk_integration.rb program. 3 | # 4 | # License: Same as for Gosu (MIT) 5 | # Created on 21/10/2007, 00:05:19 by Robert Sheehan 6 | 7 | require "gosu" 8 | require "chipmunk" 9 | require "rmagick" 10 | 11 | # Layering of sprites 12 | module ZOrder 13 | BACKGROUND, BOX = *0..1 14 | end 15 | 16 | WIDTH = 640 17 | HEIGHT = 480 18 | TICK = 1.0 / 60.0 19 | NUM_POLYGONS = 80 20 | NUM_SIDES = 4 21 | EDGE_SIZE = 15 22 | 23 | class ChipmunkAndRMagick < (Example rescue Gosu::Window) 24 | def radians_to_vec2(radians) 25 | CP::Vec2.new(Math::cos(radians), Math::sin(radians)) 26 | end 27 | 28 | def initialize 29 | super WIDTH, HEIGHT 30 | 31 | self.caption = "Chipmunk, RMagick and Gosu" 32 | 33 | @space = CP::Space.new 34 | @space.iterations = 5 35 | @space.gravity = CP::Vec2.new(0, 100) 36 | 37 | # you can replace the background with any image with this line 38 | # background = Magick::ImageList.new("media/space.png") 39 | fill = Magick::TextureFill.new(Magick::ImageList.new("granite:")) 40 | background = Magick::Image.new(WIDTH, HEIGHT, fill) 41 | setup_triangles(background) 42 | @background_image = Gosu::Image.new(background, tileable: true) # turn the image into a Gosu one 43 | @boxes = create_boxes(NUM_POLYGONS) 44 | end 45 | 46 | # Create all of the static triangles. 47 | # Adds them to the space and the background image. 48 | def setup_triangles(background) 49 | gc = Magick::Draw.new 50 | gc.stroke_width(2) 51 | gc.stroke("red") 52 | gc.fill("blue") 53 | # all the triangles are part of the same body 54 | body = CP::Body.new(Float::MAX, Float::MAX) 55 | base = 15 56 | height = 10 57 | shape_vertices = [CP::Vec2.new(-base, base), CP::Vec2.new(base, base), CP::Vec2.new(0, -height)] 58 | # make shapes and images 59 | 8.times do |i| 60 | 8.times do |j| 61 | stagger = (j % 2) * 40 62 | x = i * 80 + stagger 63 | y = j * 70 + 80 64 | shape = CP::Shape::Poly.new(body, shape_vertices, CP::Vec2.new(x, y)) 65 | shape.e = 1 66 | shape.u = 1 67 | @space.add_static_shape(shape) 68 | gc.polygon(x - base + 1, y + base - 1, x + base - 1, y + base - 1, x, y - height + 1) 69 | end 70 | end 71 | # do the drawing 72 | gc.draw(background) 73 | end 74 | 75 | # Produces the vertices of a regular polygon. 76 | def polygon_vertices(sides, size) 77 | vertices = [] 78 | sides.times do |i| 79 | angle = -2 * Math::PI * i / sides 80 | vertices << radians_to_vec2(angle) * size 81 | end 82 | return vertices 83 | end 84 | 85 | # Produces the image of a polygon. 86 | def polygon_image(vertices) 87 | box_image = Magick::Image.new(EDGE_SIZE * 2, EDGE_SIZE * 2) { self.background_color = "transparent" } 88 | gc = Magick::Draw.new 89 | gc.stroke("red") 90 | gc.fill("plum") 91 | draw_vertices = vertices.map { |v| [v.x + EDGE_SIZE, v.y + EDGE_SIZE] }.flatten 92 | gc.polygon(*draw_vertices) 93 | gc.draw(box_image) 94 | return Gosu::Image.new(box_image) 95 | end 96 | 97 | # Produces the polygon objects and adds them to the space. 98 | def create_boxes(num) 99 | box_vertices = polygon_vertices(NUM_SIDES, EDGE_SIZE) 100 | box_image = polygon_image(box_vertices) 101 | boxes = [] 102 | num.times do 103 | body = CP::Body.new(1, CP::moment_for_poly(1.0, box_vertices, CP::Vec2.new(0, 0))) # mass, moment of inertia 104 | body.p = CP::Vec2.new(rand(WIDTH), rand(40) - 50) 105 | shape = CP::Shape::Poly.new(body, box_vertices, CP::Vec2.new(0, 0)) 106 | shape.e = 0.0 107 | shape.u = 0.4 108 | boxes << Box.new(box_image, body) 109 | @space.add_body(body) 110 | @space.add_shape(shape) 111 | end 112 | return boxes 113 | end 114 | 115 | # All the simulation is done here. 116 | def update 117 | @space.step(TICK) 118 | @boxes.each { |box| box.check_off_screen } 119 | end 120 | 121 | # All the updating of the screen is done here. 122 | def draw 123 | @background_image.draw(0, 0, ZOrder::BACKGROUND) 124 | @boxes.each { |box| box.draw } 125 | end 126 | end 127 | 128 | # The falling boxes class. 129 | # Nothing more than a body and an image. 130 | class Box 131 | def initialize(image, body) 132 | @image = image 133 | @body = body 134 | end 135 | 136 | # If it goes offscreen we put it back to the top. 137 | def check_off_screen 138 | pos = @body.p 139 | if pos.y > HEIGHT + EDGE_SIZE or pos.x > WIDTH + EDGE_SIZE or pos.x < -EDGE_SIZE 140 | @body.p = CP::Vec2.new(rand * WIDTH, 0) 141 | end 142 | end 143 | 144 | def draw 145 | @image.draw_rot(@body.p.x, @body.p.y, ZOrder::BOX, @body.a.radians_to_gosu) 146 | end 147 | end 148 | 149 | ChipmunkAndRMagick.new.show if __FILE__ == $0 150 | -------------------------------------------------------------------------------- /examples/text_input.rb: -------------------------------------------------------------------------------- 1 | # This example demonstrates use of the TextInput class with three text field widgets. 2 | # One can cycle through them with tab, or click into the text fields and change their contents. 3 | 4 | # The way TextInput works is that you create an instance of it, and then assign it to the text_input 5 | # attribute of your window. 6 | # Until you set this attribute to nil again, the TextInput object will then build a string from user 7 | # input that can be accessed via TextInput#text. 8 | 9 | # The TextInput object also maintains the position of the caret, which is defined as the index of 10 | # its right neighbour character, i.e. a carent_pos of 0 is always the left-most position, and a 11 | # caret_pos of text.length is always the right-most position. 12 | # There is a second attribute called selection_start that is equal to caret_pos when there is no 13 | # selection, and otherwise defines the selected range. If you set caret_pos to a different value, 14 | # you usually want to set selection_start as well. 15 | 16 | # A TextInput object is purely abstract. Drawing the input field is left to the user. 17 | # In this example, we are subclassing TextInput to add this code, but you can also work with 18 | # composition instead of inheritance. 19 | 20 | require "gosu" 21 | 22 | class TextField < Gosu::TextInput 23 | FONT = Gosu::Font.new(20) 24 | WIDTH = 350 25 | LENGTH_LIMIT = 20 26 | PADDING = 5 27 | 28 | INACTIVE_COLOR = 0xcc_666666 29 | ACTIVE_COLOR = 0xcc_ff6666 30 | SELECTION_COLOR = 0xcc_0000ff 31 | CARET_COLOR = 0xff_ffffff 32 | 33 | attr_reader :x, :y 34 | 35 | def initialize(window, x, y) 36 | # It's important to call the inherited constructor. 37 | super() 38 | 39 | @window, @x, @y = window, x, y 40 | 41 | # Start with a self-explanatory text in each field. 42 | self.text = "Click to edit" 43 | end 44 | 45 | # In this example, we use the filter method to prevent the user from entering a text that exceeds 46 | # the length limit. However, you can also use this to blacklist certain characters, etc. 47 | def filter(new_text) 48 | allowed_length = [LENGTH_LIMIT - text.length, 0].max 49 | new_text[0, allowed_length] 50 | end 51 | 52 | def draw(z) 53 | # Change the background colour if this is the currently selected text field. 54 | if @window.text_input == self 55 | color = ACTIVE_COLOR 56 | else 57 | color = INACTIVE_COLOR 58 | end 59 | Gosu.draw_rect x - PADDING, y - PADDING, WIDTH + 2 * PADDING, height + 2 * PADDING, color, z 60 | 61 | # Calculate the position of the caret and the selection start. 62 | pos_x = x + FONT.text_width(self.text[0...self.caret_pos]) 63 | sel_x = x + FONT.text_width(self.text[0...self.selection_start]) 64 | sel_w = pos_x - sel_x 65 | 66 | # Draw the selection background, if any. (If not, sel_x and pos_x will be 67 | # the same value, making this a no-op call.) 68 | Gosu.draw_rect sel_x, y, sel_w, height, SELECTION_COLOR, z 69 | 70 | # Draw the caret if this is the currently selected field. 71 | if @window.text_input == self 72 | Gosu.draw_line pos_x, y, CARET_COLOR, pos_x, y + height, CARET_COLOR, z 73 | end 74 | 75 | # Finally, draw the text itself! 76 | FONT.draw_text self.text, x, y, z 77 | end 78 | 79 | def height 80 | FONT.height 81 | end 82 | 83 | # Hit-test for selecting a text field with the mouse. 84 | def under_mouse? 85 | @window.mouse_x > x - PADDING and @window.mouse_x < x + WIDTH + PADDING and 86 | @window.mouse_y > y - PADDING and @window.mouse_y < y + height + PADDING 87 | end 88 | 89 | # Tries to move the caret to the position specifies by mouse_x 90 | def move_caret_to_mouse 91 | # Test character by character 92 | 1.upto(self.text.length) do |i| 93 | if @window.mouse_x < x + FONT.text_width(text[0...i]) 94 | self.caret_pos = self.selection_start = i - 1 95 | return 96 | end 97 | end 98 | # Default case: user must have clicked the right edge 99 | self.caret_pos = self.selection_start = self.text.length 100 | end 101 | end 102 | 103 | class TextInputDemo < (Example rescue Gosu::Window) 104 | def initialize 105 | super 640, 480 106 | self.caption = "Text Input Demo" 107 | 108 | text = 109 | "This demo explains (in the source code) how to use the Gosu::TextInput API by building a little TextField class around it. 110 | 111 | Each text field can take up to 30 characters, and you can use Tab to switch between them. 112 | 113 | As in every example, press E to look at the source code." 114 | 115 | # Remove all leading spaces so the text is left-aligned 116 | text.gsub! /^ +/, "" 117 | 118 | @text = Gosu::Image.from_markup text, 20, width: 540 119 | 120 | # Set up an array of three text fields. 121 | @text_fields = Array.new(3) { |index| TextField.new(self, 50, 300 + index * 50) } 122 | end 123 | 124 | def needs_cursor? 125 | true 126 | end 127 | 128 | def draw 129 | @text.draw 50, 50, 0 130 | @text_fields.each { |tf| tf.draw(0) } 131 | end 132 | 133 | def button_down(id) 134 | if id == Gosu::KB_TAB 135 | # Tab key will not be 'eaten' by text fields; use for switching through 136 | # text fields. 137 | index = @text_fields.index(self.text_input) || -1 138 | self.text_input = @text_fields[(index + 1) % @text_fields.size] 139 | elsif id == Gosu::KB_ESCAPE 140 | # Escape key will not be 'eaten' by text fields; use for deselecting. 141 | if self.text_input 142 | self.text_input = nil 143 | else 144 | close 145 | end 146 | elsif id == Gosu::MS_LEFT 147 | # Mouse click: Select text field based on mouse position. 148 | self.text_input = @text_fields.find { |tf| tf.under_mouse? } 149 | # Also move caret to clicked position 150 | self.text_input.move_caret_to_mouse unless self.text_input.nil? 151 | else 152 | super 153 | end 154 | end 155 | end 156 | 157 | TextInputDemo.new.show if __FILE__ == $0 158 | -------------------------------------------------------------------------------- /examples/opengl_integration.rb: -------------------------------------------------------------------------------- 1 | # The tutorial game over a landscape rendered with OpenGL. 2 | # Basically shows how arbitrary OpenGL calls can be put into 3 | # the block given to Window#gl, and that Gosu Images can be 4 | # used as textures using the gl_tex_info call. 5 | 6 | require "gosu" 7 | require "opengl" 8 | 9 | OpenGL.load_lib 10 | 11 | WIDTH, HEIGHT = 640, 480 12 | 13 | module ZOrder 14 | Background, Stars, Player, UI = *0..3 15 | end 16 | 17 | # The only really new class here. 18 | # Draws a scrolling, repeating texture with a randomized height map. 19 | class GLBackground 20 | # Height map size 21 | POINTS_X = 7 22 | POINTS_Y = 7 23 | # Scrolling speed 24 | SCROLLS_PER_STEP = 50 25 | 26 | def initialize 27 | @image = Gosu::Image.new("media/earth.png", tileable: true) 28 | @scrolls = 0 29 | @height_map = Array.new(POINTS_Y) { Array.new(POINTS_X) { rand } } 30 | end 31 | 32 | def scroll 33 | @scrolls += 1 34 | if @scrolls == SCROLLS_PER_STEP 35 | @scrolls = 0 36 | @height_map.shift 37 | @height_map.push Array.new(POINTS_X) { rand } 38 | end 39 | end 40 | 41 | def draw(z) 42 | # gl will execute the given block in a clean OpenGL environment, then reset 43 | # everything so Gosu's rendering can take place again. 44 | Gosu.gl(z) { exec_gl } 45 | end 46 | 47 | private 48 | 49 | include OpenGL 50 | 51 | def exec_gl 52 | glClearColor(0.0, 0.2, 0.5, 1.0) 53 | glClearDepth(0) 54 | glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) 55 | 56 | # Get the name of the OpenGL texture the Image resides on, and the 57 | # u/v coordinates of the rect it occupies. 58 | # gl_tex_info can return nil if the image was too large to fit onto 59 | # a single OpenGL texture and was internally split up. 60 | info = @image.gl_tex_info 61 | return unless info 62 | 63 | # Pretty straightforward OpenGL code. 64 | 65 | glDepthFunc(GL_GEQUAL) 66 | glEnable(GL_DEPTH_TEST) 67 | glEnable(GL_BLEND) 68 | 69 | glMatrixMode(GL_PROJECTION) 70 | glLoadIdentity 71 | glFrustum(-0.10, 0.10, -0.075, 0.075, 1, 100) 72 | 73 | glMatrixMode(GL_MODELVIEW) 74 | glLoadIdentity 75 | glTranslatef(0, 0, -4) 76 | 77 | glEnable(GL_TEXTURE_2D) 78 | glBindTexture(GL_TEXTURE_2D, info.tex_name) 79 | 80 | offs_y = 1.0 * @scrolls / SCROLLS_PER_STEP 81 | 82 | 0.upto(POINTS_Y - 2) do |y| 83 | 0.upto(POINTS_X - 2) do |x| 84 | glBegin(GL_TRIANGLE_STRIP) 85 | z = @height_map[y][x] 86 | glColor4d(1, 1, 1, z) 87 | glTexCoord2d(info.left, info.top) 88 | glVertex3d(-0.5 + (x - 0.0) / (POINTS_X - 1), -0.5 + (y - offs_y - 0.0) / (POINTS_Y - 2), z) 89 | 90 | z = @height_map[y + 1][x] 91 | glColor4d(1, 1, 1, z) 92 | glTexCoord2d(info.left, info.bottom) 93 | glVertex3d(-0.5 + (x - 0.0) / (POINTS_X - 1), -0.5 + (y - offs_y + 1.0) / (POINTS_Y - 2), z) 94 | 95 | z = @height_map[y][x + 1] 96 | glColor4d(1, 1, 1, z) 97 | glTexCoord2d(info.right, info.top) 98 | glVertex3d(-0.5 + (x + 1.0) / (POINTS_X - 1), -0.5 + (y - offs_y - 0.0) / (POINTS_Y - 2), z) 99 | 100 | z = @height_map[y + 1][x + 1] 101 | glColor4d(1, 1, 1, z) 102 | glTexCoord2d(info.right, info.bottom) 103 | glVertex3d(-0.5 + (x + 1.0) / (POINTS_X - 1), -0.5 + (y - offs_y + 1.0) / (POINTS_Y - 2), z) 104 | glEnd 105 | end 106 | end 107 | end 108 | end 109 | 110 | # Roughly adapted from the tutorial game. Always faces north. 111 | class Player 112 | Speed = 7 113 | 114 | attr_reader :score 115 | 116 | def initialize(x, y) 117 | @image = Gosu::Image.new("media/starfighter.bmp") 118 | @beep = Gosu::Sample.new("media/beep.wav") 119 | @x, @y = x, y 120 | @score = 0 121 | end 122 | 123 | def move_left 124 | @x = [@x - Speed, 0].max 125 | end 126 | 127 | def move_right 128 | @x = [@x + Speed, WIDTH].min 129 | end 130 | 131 | def accelerate 132 | @y = [@y - Speed, 50].max 133 | end 134 | 135 | def brake 136 | @y = [@y + Speed, HEIGHT].min 137 | end 138 | 139 | def draw 140 | @image.draw(@x - @image.width / 2, @y - @image.height / 2, ZOrder::Player) 141 | end 142 | 143 | def collect_stars(stars) 144 | stars.reject! do |star| 145 | if Gosu.distance(@x, @y, star.x, star.y) < 35 146 | @score += 10 147 | @beep.play 148 | true 149 | else 150 | false 151 | end 152 | end 153 | end 154 | end 155 | 156 | # Also taken from the tutorial, but drawn with draw_rot and an increasing angle 157 | # for extra rotation coolness! 158 | class Star 159 | attr_reader :x, :y 160 | 161 | def initialize(animation) 162 | @animation = animation 163 | @color = Gosu::Color.new(0xff_000000) 164 | @color.red = rand(255 - 40) + 40 165 | @color.green = rand(255 - 40) + 40 166 | @color.blue = rand(255 - 40) + 40 167 | @x = rand * 800 168 | @y = 0 169 | end 170 | 171 | def draw 172 | img = @animation[Gosu.milliseconds / 100 % @animation.size] 173 | img.draw_rot(@x, @y, ZOrder::Stars, @y, 0.5, 0.5, 1, 1, @color, :add) 174 | end 175 | 176 | def update 177 | # Move towards bottom of screen 178 | @y += 3 179 | # Return false when out of screen (gets deleted then) 180 | @y < 650 181 | end 182 | end 183 | 184 | class OpenGLIntegration < (Example rescue Gosu::Window) 185 | def initialize 186 | super WIDTH, HEIGHT 187 | 188 | self.caption = "OpenGL Integration" 189 | 190 | @gl_background = GLBackground.new 191 | 192 | @player = Player.new(400, 500) 193 | 194 | @star_anim = Gosu::Image::load_tiles("media/star.png", 25, 25) 195 | @stars = Array.new 196 | 197 | @font = Gosu::Font.new(20) 198 | end 199 | 200 | def update 201 | @player.move_left if Gosu.button_down? Gosu::KB_LEFT or Gosu.button_down? Gosu::GP_LEFT 202 | @player.move_right if Gosu.button_down? Gosu::KB_RIGHT or Gosu.button_down? Gosu::GP_RIGHT 203 | @player.accelerate if Gosu.button_down? Gosu::KB_UP or Gosu.button_down? Gosu::GP_UP 204 | @player.brake if Gosu.button_down? Gosu::KB_DOWN or Gosu.button_down? Gosu::GP_DOWN 205 | 206 | @player.collect_stars(@stars) 207 | 208 | @stars.reject! { |star| !star.update } 209 | 210 | @gl_background.scroll 211 | 212 | @stars.push(Star.new(@star_anim)) if rand(20) == 0 213 | end 214 | 215 | def draw 216 | @player.draw 217 | @stars.each { |star| star.draw } 218 | @font.draw_text("Score: #{@player.score}", 10, 10, ZOrder::UI, 1.0, 1.0, 0xff_ffff00) 219 | @gl_background.draw(ZOrder::Background) 220 | end 221 | end 222 | 223 | OpenGLIntegration.new.show if __FILE__ == $0 224 | -------------------------------------------------------------------------------- /examples/cptn_ruby.rb: -------------------------------------------------------------------------------- 1 | # A simple jump-and-run/platformer game with a tile-based map. 2 | 3 | # Shows how to 4 | # * implement jumping/gravity 5 | # * implement scrolling using Window#translate 6 | # * implement a simple tile-based map 7 | # * load levels from primitive text files 8 | 9 | # Some exercises, starting at the real basics: 10 | # 0) understand the existing code! 11 | # As shown in the tutorial: 12 | # 1) change it use Gosu's Z-ordering 13 | # 2) add gamepad support 14 | # 3) add a score as in the tutorial game 15 | # 4) similarly, add sound effects for various events 16 | # Exploring this game's code and Gosu: 17 | # 5) make the player wider, so he doesn't fall off edges as easily 18 | # 6) add background music (check if playing in Window#update to implement 19 | # looping) 20 | # 7) implement parallax scrolling for the star background! 21 | # Getting tricky: 22 | # 8) optimize Map#draw so only tiles on screen are drawn (needs modulo, a pen 23 | # and paper to figure out) 24 | # 9) add loading of next level when all gems are collected 25 | # ...Enemies, a more sophisticated object system, weapons, title and credits 26 | # screens... 27 | 28 | require "gosu" 29 | 30 | WIDTH, HEIGHT = 640, 480 31 | 32 | module Tiles 33 | Grass = 0 34 | Earth = 1 35 | end 36 | 37 | class CollectibleGem 38 | attr_reader :x, :y 39 | 40 | def initialize(image, x, y) 41 | @image = image 42 | @x, @y = x, y 43 | end 44 | 45 | def draw 46 | # Draw, slowly rotating 47 | @image.draw_rot(@x, @y, 0, 25 * Math.sin(Gosu.milliseconds / 133.7)) 48 | end 49 | end 50 | 51 | # Player class. 52 | class Player 53 | attr_reader :x, :y 54 | 55 | def initialize(map, x, y) 56 | @x, @y = x, y 57 | @dir = :left 58 | @vy = 0 # Vertical velocity 59 | @map = map 60 | # Load all animation frames 61 | @standing, @walk1, @walk2, @jump = *Gosu::Image.load_tiles("media/cptn_ruby.png", 50, 50) 62 | # This always points to the frame that is currently drawn. 63 | # This is set in update, and used in draw. 64 | @cur_image = @standing 65 | end 66 | 67 | def draw 68 | # Flip vertically when facing to the left. 69 | if @dir == :left 70 | offs_x = -25 71 | factor = 1.0 72 | else 73 | offs_x = 25 74 | factor = -1.0 75 | end 76 | @cur_image.draw(@x + offs_x, @y - 49, 0, factor, 1.0) 77 | end 78 | 79 | # Could the object be placed at x + offs_x/y + offs_y without being stuck? 80 | def would_fit(offs_x, offs_y) 81 | # Check at the center/top and center/bottom for map collisions 82 | not @map.solid?(@x + offs_x, @y + offs_y) and 83 | not @map.solid?(@x + offs_x, @y + offs_y - 45) 84 | end 85 | 86 | def update(move_x) 87 | # Select image depending on action 88 | if (move_x == 0) 89 | @cur_image = @standing 90 | else 91 | @cur_image = (Gosu.milliseconds / 175 % 2 == 0) ? @walk1 : @walk2 92 | end 93 | if (@vy < 0) 94 | @cur_image = @jump 95 | end 96 | 97 | # Directional walking, horizontal movement 98 | if move_x > 0 99 | @dir = :right 100 | move_x.times { if would_fit(1, 0) then @x += 1 end } 101 | end 102 | if move_x < 0 103 | @dir = :left 104 | (-move_x).times { if would_fit(-1, 0) then @x -= 1 end } 105 | end 106 | 107 | # Acceleration/gravity 108 | # By adding 1 each frame, and (ideally) adding vy to y, the player's 109 | # jumping curve will be the parabole we want it to be. 110 | @vy += 1 111 | # Vertical movement 112 | if @vy > 0 113 | @vy.times { if would_fit(0, 1) then @y += 1 else @vy = 0 end } 114 | end 115 | if @vy < 0 116 | (-@vy).times { if would_fit(0, -1) then @y -= 1 else @vy = 0 end } 117 | end 118 | end 119 | 120 | def try_to_jump 121 | if @map.solid?(@x, @y + 1) 122 | @vy = -20 123 | end 124 | end 125 | 126 | def collect_gems(gems) 127 | # Same as in the tutorial game. 128 | gems.reject! do |c| 129 | (c.x - @x).abs < 50 and (c.y - @y).abs < 50 130 | end 131 | end 132 | end 133 | 134 | # Map class holds and draws tiles and gems. 135 | class Map 136 | attr_reader :width, :height, :gems 137 | 138 | def initialize(filename) 139 | # Load 60x60 tiles, 5px overlap in all four directions. 140 | @tileset = Gosu::Image.load_tiles("media/tileset.png", 60, 60, tileable: true) 141 | 142 | gem_img = Gosu::Image.new("media/gem.png") 143 | @gems = [] 144 | 145 | lines = File.readlines(filename).map { |line| line.chomp } 146 | @height = lines.size 147 | @width = lines[0].size 148 | @tiles = Array.new(@width) do |x| 149 | Array.new(@height) do |y| 150 | case lines[y][x, 1] 151 | when '"' 152 | Tiles::Grass 153 | when "#" 154 | Tiles::Earth 155 | when "x" 156 | @gems.push(CollectibleGem.new(gem_img, x * 50 + 25, y * 50 + 25)) 157 | nil 158 | else 159 | nil 160 | end 161 | end 162 | end 163 | end 164 | 165 | def draw 166 | # Very primitive drawing function: 167 | # Draws all the tiles, some off-screen, some on-screen. 168 | @height.times do |y| 169 | @width.times do |x| 170 | tile = @tiles[x][y] 171 | if tile 172 | # Draw the tile with an offset (tile images have some overlap) 173 | # Scrolling is implemented here just as in the game objects. 174 | @tileset[tile].draw(x * 50 - 5, y * 50 - 5, 0) 175 | end 176 | end 177 | end 178 | @gems.each { |c| c.draw } 179 | end 180 | 181 | # Solid at a given pixel position? 182 | def solid?(x, y) 183 | y < 0 || @tiles[x / 50][y / 50] 184 | end 185 | end 186 | 187 | class CptnRuby < (Example rescue Gosu::Window) 188 | def initialize 189 | super WIDTH, HEIGHT 190 | 191 | self.caption = "Cptn. Ruby" 192 | 193 | @sky = Gosu::Image.new("media/space.png", tileable: true) 194 | @map = Map.new("media/cptn_ruby_map.txt") 195 | @cptn = Player.new(@map, 400, 100) 196 | # The scrolling position is stored as top left corner of the screen. 197 | @camera_x = @camera_y = 0 198 | end 199 | 200 | def update 201 | move_x = 0 202 | move_x -= 5 if Gosu.button_down? Gosu::KB_LEFT 203 | move_x += 5 if Gosu.button_down? Gosu::KB_RIGHT 204 | @cptn.update(move_x) 205 | @cptn.collect_gems(@map.gems) 206 | # Scrolling follows player 207 | @camera_x = [[@cptn.x - WIDTH / 2, 0].max, @map.width * 50 - WIDTH].min 208 | @camera_y = [[@cptn.y - HEIGHT / 2, 0].max, @map.height * 50 - HEIGHT].min 209 | end 210 | 211 | def draw 212 | @sky.draw 0, 0, 0 213 | Gosu.translate(-@camera_x, -@camera_y) do 214 | @map.draw 215 | @cptn.draw 216 | end 217 | end 218 | 219 | def button_down(id) 220 | case id 221 | when Gosu::KB_UP 222 | @cptn.try_to_jump 223 | when Gosu::KB_ESCAPE 224 | close 225 | else 226 | super 227 | end 228 | end 229 | end 230 | 231 | CptnRuby.new.show if __FILE__ == $0 232 | -------------------------------------------------------------------------------- /examples/chipmunk_integration.rb: -------------------------------------------------------------------------------- 1 | ## File: ChipmunkIntegration.rb 2 | ## Author: Dirk Johnson 3 | ## Version: 1.0.0 4 | ## Date: 2007-10-05 5 | ## License: Same as for Gosu (MIT) 6 | ## Comments: Based on the Gosu Ruby Tutorial, but incorporating the Chipmunk Physics Engine 7 | ## See https://github.com/jlnr/gosu/wiki/Ruby-Chipmunk-Integration for the accompanying text. 8 | 9 | require "gosu" 10 | require "chipmunk" 11 | 12 | WIDTH = 640 13 | HEIGHT = 480 14 | 15 | # The number of steps to process every Gosu update 16 | # The Player ship can get going so fast as to "move through" a 17 | # star without triggering a collision; an increased number of 18 | # Chipmunk step calls per update will effectively avoid this issue 19 | SUBSTEPS = 6 20 | 21 | # Layering of objects 22 | module ZOrder 23 | Background, Stars, Player, UI = *0..3 24 | end 25 | 26 | # This game will have one Player in the form of a ship 27 | class Player 28 | attr_reader :shape 29 | 30 | def initialize(shape) 31 | @image = Gosu::Image.new("media/starfighter.bmp") 32 | @shape = shape 33 | @shape.body.p = CP::Vec2.new(0.0, 0.0) # position 34 | @shape.body.v = CP::Vec2.new(0.0, 0.0) # velocity 35 | 36 | # Keep in mind that down the screen is positive y, which means that PI/2 radians, 37 | # which you might consider the top in the traditional Trig unit circle sense is actually 38 | # the bottom; thus 3PI/2 is the top 39 | @shape.body.a = (3 * Math::PI / 2.0) # angle in radians; faces towards top of screen 40 | end 41 | 42 | # Directly set the position of our Player 43 | def warp(vect) 44 | @shape.body.p = vect 45 | end 46 | 47 | # Apply negative Torque; Chipmunk will do the rest 48 | # SUBSTEPS is used as a divisor to keep turning rate constant 49 | # even if the number of steps per update are adjusted 50 | def turn_left 51 | @shape.body.t -= 400.0 / SUBSTEPS 52 | end 53 | 54 | # Apply positive Torque; Chipmunk will do the rest 55 | # SUBSTEPS is used as a divisor to keep turning rate constant 56 | # even if the number of steps per update are adjusted 57 | def turn_right 58 | @shape.body.t += 400.0 / SUBSTEPS 59 | end 60 | 61 | # Apply forward force; Chipmunk will do the rest 62 | # SUBSTEPS is used as a divisor to keep acceleration rate constant 63 | # even if the number of steps per update are adjusted 64 | # Here we must convert the angle (facing) of the body into 65 | # forward momentum by creating a vector in the direction of the facing 66 | # and with a magnitude representing the force we want to apply 67 | def accelerate 68 | @shape.body.apply_force(@shape.body.rot * (3000.0 / SUBSTEPS), CP::Vec2.new(0.0, 0.0)) 69 | end 70 | 71 | # Apply even more forward force 72 | # See accelerate for more details 73 | def boost 74 | @shape.body.apply_force(@shape.body.rot * (3000.0), CP::Vec2.new(0.0, 0.0)) 75 | end 76 | 77 | # Apply reverse force 78 | # See accelerate for more details 79 | def reverse 80 | @shape.body.apply_force(-@shape.body.rot * (1000.0 / SUBSTEPS), CP::Vec2.new(0.0, 0.0)) 81 | end 82 | 83 | # Wrap to the other side of the screen when we fly off the edge 84 | def validate_position 85 | l_position = CP::Vec2.new(@shape.body.p.x % WIDTH, @shape.body.p.y % HEIGHT) 86 | @shape.body.p = l_position 87 | end 88 | 89 | def draw 90 | @image.draw_rot(@shape.body.p.x, @shape.body.p.y, ZOrder::Player, @shape.body.a.radians_to_gosu) 91 | end 92 | end 93 | 94 | # See how simple our Star is? 95 | # Of course... it just sits around and looks good... 96 | class Star 97 | attr_reader :shape 98 | 99 | def initialize(animation, shape) 100 | @animation = animation 101 | @color = Gosu::Color.new(0xff_000000) 102 | @color.red = rand(255 - 40) + 40 103 | @color.green = rand(255 - 40) + 40 104 | @color.blue = rand(255 - 40) + 40 105 | @shape = shape 106 | @shape.body.p = CP::Vec2.new(rand * WIDTH, rand * HEIGHT) # position 107 | @shape.body.v = CP::Vec2.new(0.0, 0.0) # velocity 108 | @shape.body.a = 0.gosu_to_radians # faces towards top of screen 109 | end 110 | 111 | def draw 112 | img = @animation[Gosu.milliseconds / 100 % @animation.size] 113 | img.draw(@shape.body.p.x - img.width / 2.0, @shape.body.p.y - img.height / 2.0, ZOrder::Stars, 1, 1, @color, :add) 114 | end 115 | end 116 | 117 | # The Gosu::Window is always the "environment" of our game 118 | # It also provides the pulse of our game 119 | class ChipmunkIntegration < (Example rescue Gosu::Window) 120 | def initialize 121 | super WIDTH, HEIGHT 122 | 123 | self.caption = "Gosu & Chipmunk Integration Demo" 124 | 125 | @background_image = Gosu::Image.new("media/space.png", tileable: true) 126 | 127 | # Put the beep here, as it is the environment now that determines collision 128 | @beep = Gosu::Sample.new("media/beep.wav") 129 | 130 | # Put the score here, as it is the environment that tracks this now 131 | @score = 0 132 | @font = Gosu::Font.new(20) 133 | 134 | # Time increment over which to apply a physics "step" ("delta t") 135 | @dt = (1.0 / 60.0) 136 | 137 | # Create our Space and set its damping 138 | # A damping of 0.8 causes the ship bleed off its force and torque over time 139 | # This is not realistic behavior in a vacuum of space, but it gives the game 140 | # the feel I'd like in this situation 141 | @space = CP::Space.new 142 | @space.damping = 0.8 143 | 144 | # Create the Body for the Player 145 | body = CP::Body.new(10.0, 150.0) 146 | 147 | # In order to create a shape, we must first define it 148 | # Chipmunk defines 3 types of Shapes: Segments, Circles and Polys 149 | # We'll use s simple, 4 sided Poly for our Player (ship) 150 | # You need to define the vectors so that the "top" of the Shape is towards 0 radians (the right) 151 | shape_array = [CP::Vec2.new(-25.0, -25.0), CP::Vec2.new(-25.0, 25.0), CP::Vec2.new(25.0, 1.0), CP::Vec2.new(25.0, -1.0)] 152 | shape = CP::Shape::Poly.new(body, shape_array, CP::Vec2.new(0, 0)) 153 | 154 | # The collision_type of a shape allows us to set up special collision behavior 155 | # based on these types. The actual value for the collision_type is arbitrary 156 | # and, as long as it is consistent, will work for us; of course, it helps to have it make sense 157 | shape.collision_type = :ship 158 | 159 | @space.add_body(body) 160 | @space.add_shape(shape) 161 | 162 | @player = Player.new(shape) 163 | @player.warp(CP::Vec2.new(320, 240)) # move to the center of the window 164 | 165 | @star_anim = Gosu::Image.load_tiles("media/star.png", 25, 25) 166 | @stars = Array.new 167 | 168 | # Here we define what is supposed to happen when a Player (ship) collides with a Star 169 | # I create a @remove_shapes array because we cannot remove either Shapes or Bodies 170 | # from Space within a collision closure, rather, we have to wait till the closure 171 | # is through executing, then we can remove the Shapes and Bodies 172 | # In this case, the Shapes and the Bodies they own are removed in the Gosu::Window.update phase 173 | # by iterating over the @remove_shapes array 174 | # Also note that both Shapes involved in the collision are passed into the closure 175 | # in the same order that their collision_types are defined in the add_collision_func call 176 | @remove_shapes = [] 177 | @space.add_collision_func(:ship, :star) do |ship_shape, star_shape| 178 | @score += 10 179 | @beep.play 180 | @remove_shapes << star_shape 181 | end 182 | 183 | # Here we tell Space that we don't want one star bumping into another 184 | # The reason we need to do this is because when the Player hits a Star, 185 | # the Star will travel until it is removed in the update cycle below 186 | # which means it may collide and therefore push other Stars 187 | # To see the effect, remove this line and play the game, every once in a while 188 | # you'll see a Star moving 189 | @space.add_collision_func(:star, :star, &nil) 190 | end 191 | 192 | def update 193 | # Step the physics environment SUBSTEPS times each update 194 | SUBSTEPS.times do 195 | # This iterator makes an assumption of one Shape per Star making it safe to remove 196 | # each Shape's Body as it comes up 197 | # If our Stars had multiple Shapes, as would be required if we were to meticulously 198 | # define their true boundaries, we couldn't do this as we would remove the Body 199 | # multiple times 200 | # We would probably solve this by creating a separate @remove_bodies array to remove the Bodies 201 | # of the Stars that were gathered by the Player 202 | @remove_shapes.each do |shape| 203 | @stars.delete_if { |star| star.shape == shape } 204 | @space.remove_body(shape.body) 205 | @space.remove_shape(shape) 206 | end 207 | @remove_shapes.clear # clear out the shapes for next pass 208 | 209 | # When a force or torque is set on a Body, it is cumulative 210 | # This means that the force you applied last SUBSTEP will compound with the 211 | # force applied this SUBSTEP; which is probably not the behavior you want 212 | # We reset the forces on the Player each SUBSTEP for this reason 213 | @player.shape.body.reset_forces 214 | 215 | # Wrap around the screen to the other side 216 | @player.validate_position 217 | 218 | # Check keyboard 219 | if Gosu.button_down? Gosu::KB_LEFT 220 | @player.turn_left 221 | end 222 | if Gosu.button_down? Gosu::KB_RIGHT 223 | @player.turn_right 224 | end 225 | 226 | if Gosu.button_down? Gosu::KB_UP 227 | if Gosu.button_down?(Gosu::KB_RIGHT_SHIFT) or Gosu.button_down?(Gosu::KB_LEFT_SHIFT) 228 | @player.boost 229 | else 230 | @player.accelerate 231 | end 232 | elsif Gosu.button_down? Gosu::KB_DOWN 233 | @player.reverse 234 | end 235 | 236 | # Perform the step over @dt period of time 237 | # For best performance @dt should remain consistent for the game 238 | @space.step(@dt) 239 | end 240 | 241 | # Each update (not SUBSTEP) we see if we need to add more Stars 242 | if rand(100) < 4 and @stars.size < 25 243 | body = CP::Body.new(0.0001, 0.0001) 244 | shape = CP::Shape::Circle.new(body, 25 / 2, CP::Vec2.new(0.0, 0.0)) 245 | shape.collision_type = :star 246 | 247 | @space.add_body(body) 248 | @space.add_shape(shape) 249 | 250 | @stars.push(Star.new(@star_anim, shape)) 251 | end 252 | end 253 | 254 | def draw 255 | @background_image.draw(0, 0, ZOrder::Background) 256 | @player.draw 257 | @stars.each { |star| star.draw } 258 | @font.draw_text("Score: #{@score}", 10, 10, ZOrder::UI, 1.0, 1.0, 0xff_ffff00) 259 | end 260 | 261 | def button_down(id) 262 | if id == Gosu::KB_ESCAPE 263 | close 264 | else 265 | super 266 | end 267 | end 268 | end 269 | 270 | ChipmunkIntegration.new.show if __FILE__ == $0 271 | -------------------------------------------------------------------------------- /examples/rmagick_integration.rb: -------------------------------------------------------------------------------- 1 | # A simple Gorilla-style shooter for two players. 2 | # Shows how Gosu and RMagick can be used together to generate a map, implement 3 | # a dynamic landscape and generally look great. 4 | # Also shows a very minimal, yet effective way of designing a game's object system. 5 | 6 | # Doesn't make use of Gosu's Z-ordering. Not many different things to draw, it's 7 | # easy to get the order right without it. 8 | 9 | # Known issues: 10 | # * Collision detection of the missiles is lazy, allows shooting through thin walls. 11 | # * The look of dead soldiers is, err, by accident. Soldier.png needs to be 12 | # designed in a less obfuscated way :) 13 | 14 | require "gosu" 15 | require "rmagick" 16 | 17 | WIDTH, HEIGHT = 640, 480 18 | 19 | NULL_PIXEL = Magick::Pixel.from_color("none") 20 | 21 | # The class for this game's map. 22 | # Design: 23 | # * Dynamic map creation at startup, holding it as RMagick Image in @image 24 | # * Testing for solidity by testing @image's pixel values 25 | # * Drawing from a Gosu::Image instance 26 | # * Blasting holes into the map is implemented by drawing and erasing portions 27 | # of @image, then recreating the corresponding area in the Gosu::Image 28 | 29 | class Map 30 | def initialize 31 | # Let's start with something simple and load the sky via RMagick. 32 | # Loading SVG files isn't possible with Gosu, so say wow! 33 | # (Seems to take a while though) 34 | sky = Magick::Image.read("media/landscape.svg").first 35 | @sky = Gosu::Image.new(sky, tileable: true) 36 | 37 | # Create the map an stores the RMagick image in @image 38 | create_rmagick_map 39 | 40 | # Copy the RMagick Image to a Gosu Image (still unchanged) 41 | @gosu_image = Gosu::Image.new(@image, tileable: true) 42 | end 43 | 44 | def solid?(x, y) 45 | # Map is open at the top. 46 | return false if y < 0 47 | # Map is closed on all other sides. 48 | return true if x < 0 or x >= WIDTH or y >= HEIGHT 49 | # Inside of the map, determine solidity from the map image. 50 | @image.pixel_color(x, y) != NULL_PIXEL 51 | end 52 | 53 | def draw 54 | # Sky background. 55 | @sky.draw 0, 0, 0 56 | # The landscape. 57 | @gosu_image.draw 0, 0, 0 58 | end 59 | 60 | # Radius of a crater. 61 | RADIUS = 25 62 | # Radius of a crater, Shadow included. 63 | SH_RADIUS = 45 64 | 65 | # Create the crater image (basically a circle shape that is used to erase 66 | # parts of the map) and the crater shadow image. 67 | CRATER_IMAGE = begin 68 | crater = Magick::Image.new(2 * RADIUS, 2 * RADIUS) { self.background_color = "none" } 69 | gc = Magick::Draw.new 70 | gc.fill("black").circle(RADIUS, RADIUS, RADIUS, 0) 71 | gc.draw crater 72 | crater 73 | end 74 | CRATER_SHADOW = CRATER_IMAGE.shadow(0, 0, (SH_RADIUS - RADIUS) / 2, 1) 75 | 76 | def blast(x, y) 77 | # Draw the shadow (twice for more intensity), then erase a circle from the map. 78 | @image.composite! CRATER_SHADOW, x - SH_RADIUS, y - SH_RADIUS, Magick::AtopCompositeOp 79 | @image.composite! CRATER_SHADOW, x - SH_RADIUS, y - SH_RADIUS, Magick::AtopCompositeOp 80 | @image.composite! CRATER_IMAGE, x - RADIUS, y - RADIUS, Magick::DstOutCompositeOp 81 | 82 | # Isolate the affected portion of the RMagick image. 83 | dirty_portion = @image.crop(x - SH_RADIUS, y - SH_RADIUS, SH_RADIUS * 2, SH_RADIUS * 2) 84 | # Overwrite this part of the Gosu image. If the crater begins outside of the map, still 85 | # just update the inner part. 86 | @gosu_image.insert dirty_portion, [x - SH_RADIUS, 0].max, [y - SH_RADIUS, 0].max 87 | end 88 | 89 | private 90 | 91 | def create_rmagick_map 92 | # This is the one large RMagick image that represents the map. 93 | @image = Magick::Image.new(WIDTH, HEIGHT) { self.background_color = "none" } 94 | 95 | # Set up a Draw object that fills with an earth texture. 96 | earth = Magick::Image.read("media/earth.png").first.resize(1.5) 97 | gc = Magick::Draw.new 98 | gc.pattern("earth", 0, 0, earth.columns, earth.rows) { gc.composite(0, 0, 0, 0, earth) } 99 | gc.fill("earth") 100 | gc.stroke("#603000").stroke_width(1.5) 101 | # Draw a smooth bezier island onto the map! 102 | polypoints = [0, HEIGHT] 103 | 0.upto(8) do |x| 104 | polypoints += [x * 100, HEIGHT * 0.2 + rand(HEIGHT * 0.8)] 105 | end 106 | polypoints += [WIDTH, HEIGHT] 107 | gc.bezier(*polypoints) 108 | gc.draw(@image) 109 | 110 | # Create a bright-dark gradient fill, an image from it and change the map's 111 | # brightness with it. 112 | fill = Magick::GradientFill.new(0, HEIGHT * 0.4, WIDTH, HEIGHT * 0.4, "#fff", "#666") 113 | gradient = Magick::Image.new(WIDTH, HEIGHT, fill) 114 | gradient = @image.composite(gradient, 0, 0, Magick::InCompositeOp) 115 | @image.composite!(gradient, 0, 0, Magick::MultiplyCompositeOp) 116 | 117 | # Finally, place the star in the middle of the map, just onto the ground. 118 | star = Magick::Image.read("media/large_star.png").first 119 | star_y = 0 120 | star_y += 20 until solid?(WIDTH / 2, star_y) 121 | @image.composite!(star, (WIDTH - star.columns) / 2, star_y - star.rows * 0.85, 122 | Magick::DstOverCompositeOp) 123 | end 124 | end 125 | 126 | # Player class. 127 | # Note that applies to the whole game: 128 | # All objects implement an informal interface. 129 | # draw: Draws the object (obviously) 130 | # update: Moves the object etc., returns false if the object is to be deleted 131 | # hit_by?(missile): Returns true if an object is hit by the missile, causing 132 | # it to explode on this object. 133 | 134 | class Player 135 | # Magic numbers considered harmful! This is the height of the 136 | # player as used for collision detection. 137 | HEIGHT = 14 138 | 139 | attr_reader :x, :y, :dead 140 | 141 | def initialize(window, x, y, color) 142 | # Only load the images once for all instances of this class. 143 | @@images ||= Gosu::Image.load_tiles("media/soldier.png", 40, 50) 144 | 145 | @window, @x, @y, @color = window, x, y, color 146 | @vy = 0 147 | 148 | # -1: left, +1: right 149 | @dir = -1 150 | 151 | # Aiming angle. 152 | @angle = 90 153 | end 154 | 155 | def draw 156 | if dead 157 | # Poor, broken soldier. 158 | @@images[0].draw_rot(x, y, 0, 290 * @dir, 0.5, 0.65, @dir * 0.5, 0.5, @color) 159 | @@images[2].draw_rot(x, y, 0, 160 * @dir, 0.95, 0.5, 0.5, @dir * 0.5, @color) 160 | else 161 | # Was moved last frame? 162 | if @show_walk_anim 163 | # Yes: Display walking animation. 164 | frame = Gosu.milliseconds / 200 % 2 165 | else 166 | # No: Stand around (boring). 167 | frame = 0 168 | end 169 | 170 | # Draw feet, then chest. 171 | @@images[frame].draw(x - 10 * @dir, y - 20, 0, @dir * 0.5, 0.5, @color) 172 | angle = @angle 173 | angle = 180 - angle if @dir == -1 174 | @@images[2].draw_rot(x, y - 5, 0, angle, 1, 0.5, 0.5, @dir * 0.5, @color) 175 | end 176 | end 177 | 178 | def update 179 | # First, assume that no walking happened this frame. 180 | @show_walk_anim = false 181 | 182 | # Gravity. 183 | @vy += 1 184 | 185 | if @vy > 1 186 | # Move upwards until hitting something. 187 | @vy.times do 188 | if @window.map.solid?(x, y + 1) 189 | @vy = 0 190 | break 191 | else 192 | @y += 1 193 | end 194 | end 195 | else 196 | # Move downwards until hitting something. 197 | (-@vy).times do 198 | if @window.map.solid?(x, y - HEIGHT - 1) 199 | @vy = 0 200 | break 201 | else 202 | @y -= 1 203 | end 204 | end 205 | end 206 | 207 | # Soldiers are never deleted (they may die, but that is a different thing). 208 | true 209 | end 210 | 211 | def aim_up 212 | @angle -= 2 unless @angle < 10 213 | end 214 | 215 | def aim_down 216 | @angle += 2 unless @angle > 170 217 | end 218 | 219 | def try_walk(dir) 220 | @show_walk_anim = true 221 | @dir = dir 222 | # First, magically move up (so soldiers can run up hills) 223 | 2.times { @y -= 1 unless @window.map.solid?(x, y - HEIGHT - 1) } 224 | # Now move into the desired direction. 225 | @x += dir unless @window.map.solid?(x + dir, y) or 226 | @window.map.solid?(x + dir, y - HEIGHT) 227 | # To make up for unnecessary movement upwards, sink downward again. 228 | 2.times { @y += 1 unless @window.map.solid?(x, y + 1) } 229 | end 230 | 231 | def try_jump 232 | @vy = -12 if @window.map.solid?(x, y + 1) 233 | end 234 | 235 | def shoot 236 | @window.objects << Missile.new(@window, x + 10 * @dir, y - 10, @angle * @dir) 237 | end 238 | 239 | def hit_by?(missile) 240 | if Gosu.distance(missile.x, missile.y, x, y) < 30 241 | # Was hit :( 242 | @dead = true 243 | return true 244 | else 245 | return false 246 | end 247 | end 248 | end 249 | 250 | # Implements the same interface as Player, except it's a missile! 251 | 252 | class Missile 253 | attr_reader :x, :y, :vx, :vy 254 | 255 | # All missile instances use the same sound. 256 | EXPLOSION = Gosu::Sample.new("media/explosion.wav") 257 | 258 | def initialize(window, x, y, angle) 259 | # Horizontal/vertical velocity. 260 | @vx, @vy = Gosu.offset_x(angle, 20).to_i, Gosu.offset_y(angle, 20).to_i 261 | 262 | @window, @x, @y = window, x + @vx, y + @vy 263 | end 264 | 265 | def update 266 | # Movement, gravity 267 | @x += @vx 268 | @y += @vy 269 | @vy += 1 270 | # Hit anything? 271 | if @window.map.solid?(x, y) or @window.objects.any? { |o| o.hit_by?(self) } 272 | # Create great particles. 273 | 5.times { @window.objects << Particle.new(@window, x - 25 + rand(51), y - 25 + rand(51)) } 274 | @window.map.blast(x, y) 275 | # Weeee, stereo sound! 276 | EXPLOSION.play_pan((1.0 * @x / WIDTH) * 2 - 1) 277 | return false 278 | else 279 | return true 280 | end 281 | end 282 | 283 | def draw 284 | # Just draw a small rectangle. 285 | Gosu.draw_rect x - 2, y - 2, 4, 4, 0xff_800000 286 | end 287 | 288 | def hit_by?(missile) 289 | # Missiles can't be hit by other missiles! 290 | false 291 | end 292 | end 293 | 294 | # Very minimal object that just draws a fading particle. 295 | 296 | class Particle 297 | def initialize(window, x, y) 298 | # All Particle instances use the same image 299 | @@image ||= Gosu::Image.new("media/smoke.png") 300 | 301 | @x, @y = x, y 302 | @color = Gosu::Color.new(255, 255, 255, 255) 303 | end 304 | 305 | def update 306 | @y -= 5 307 | @x = @x - 1 + rand(3) 308 | @color.alpha -= 5 309 | 310 | # Remove if faded completely. 311 | @color.alpha > 0 312 | end 313 | 314 | def draw 315 | @@image.draw(@x - 25, @y - 25, 0, 1, 1, @color) 316 | end 317 | 318 | def hit_by?(missile) 319 | # Smoke can't be hit! 320 | false 321 | end 322 | end 323 | 324 | # Finally, the class that ties it all together. 325 | # Very straightforward implementation. 326 | 327 | class RMagickIntegration < (Example rescue Gosu::Window) 328 | attr_reader :map, :objects 329 | 330 | def initialize 331 | super WIDTH, HEIGHT 332 | 333 | self.caption = "RMagick Integration Demo" 334 | 335 | # Texts to display in the appropriate situations. 336 | @player_instructions = [] 337 | @player_won_messages = [] 338 | 2.times do |plr| 339 | @player_instructions << Gosu::Image.from_text( 340 | "It is the #{plr == 0 ? "green" : "red"} toy soldier's turn.\n" + 341 | "(Arrow keys to walk and aim, Return to jump, Space to shoot)", 342 | 30, width: width, align: :center, 343 | ) 344 | @player_won_messages << Gosu::Image.from_text( 345 | "The #{plr == 0 ? "green" : "red"} toy soldier has won!", 346 | 30, width: width, align: :center, 347 | ) 348 | end 349 | 350 | # Create everything! 351 | @map = Map.new 352 | @players = [Player.new(self, 100, 40, 0xff_308000), Player.new(self, WIDTH - 100, 40, 0xff_803000)] 353 | @objects = @players.dup 354 | 355 | # Let any player start. 356 | @current_player = rand(2) 357 | # Currently not waiting for a missile to hit something. 358 | @waiting = false 359 | end 360 | 361 | def draw 362 | # Draw the main game. 363 | @map.draw 364 | @objects.each { |o| o.draw } 365 | 366 | # If any text should be displayed, draw it - and add a nice black border around it 367 | # by drawing it four times, with a little offset in each direction. 368 | 369 | cur_text = @player_instructions[@current_player] if not @waiting 370 | cur_text = @player_won_messages[1 - @current_player] if @players[@current_player].dead 371 | 372 | if cur_text 373 | x, y = 0, 30 374 | cur_text.draw(x - 1, y, 0, 1, 1, 0xff_000000) 375 | cur_text.draw(x + 1, y, 0, 1, 1, 0xff_000000) 376 | cur_text.draw(x, y - 1, 0, 1, 1, 0xff_000000) 377 | cur_text.draw(x, y + 1, 0, 1, 1, 0xff_000000) 378 | cur_text.draw(x, y, 0, 1, 1, 0xff_ffffff) 379 | end 380 | end 381 | 382 | def update 383 | # if waiting for the next player's turn, continue to do so until the missile has 384 | # hit something. 385 | @waiting &&= !@objects.grep(Missile).empty? 386 | 387 | # Remove all objects whose update method returns false. 388 | @objects.reject! { |o| o.update == false } 389 | 390 | # If it's a player's turn, forward controls. 391 | if not @waiting and not @players[@current_player].dead 392 | player = @players[@current_player] 393 | player.aim_up if Gosu.button_down? Gosu::KB_UP 394 | player.aim_down if Gosu.button_down? Gosu::KB_DOWN 395 | player.try_walk(-1) if Gosu.button_down? Gosu::KB_LEFT 396 | player.try_walk(+1) if Gosu.button_down? Gosu::KB_RIGHT 397 | player.try_jump if Gosu.button_down? Gosu::KB_RETURN 398 | end 399 | end 400 | 401 | def button_down(id) 402 | if id == Gosu::KB_SPACE and not @waiting and not @players[@current_player].dead 403 | # Shoot! This is handled in button_down because holding space shouldn't auto-fire. 404 | @players[@current_player].shoot 405 | @current_player = 1 - @current_player 406 | @waiting = true 407 | else 408 | super 409 | end 410 | end 411 | end 412 | 413 | # So far we have only defined how everything *should* work - now set it up and run it! 414 | RMagickIntegration.new.show if __FILE__ == $0 415 | --------------------------------------------------------------------------------