├── 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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------