I played with DragonRuby GTK (Game Toolkit)

The other day, I learned about the following charity projects on Twitter.

[A campaign to play 1000 indie games with a donation of at least $ 5 has started. As part of the anti-racism movement --Netorabo](https://nlab.itmedia.co.jp/nl/articles/2006/10/news135 .html)

I don't play PC games very much, but it contains materials, and when I was looking at it, I thought I should buy it if I donated it ... The letters ** Dragon Ruby GTK ** were there.

It seems that you can make cross-platform games with Ruby. (RubyMotion, which was predominant all at once, is now in this family)

This seems to be interesting, so I immediately popped it and played a little on the weekend.

Below is a record of trying the macOS version.

Hello World

Let's move it first.

$ unzip dragonruby-gtk-macos.zip 
$ cd dragonruby-macos/
$ ./dragonruby
# ./Same for dragonruby mygame
Screen Shot 2020-06-14 at 22.55.54.png

It worked. mygame / app / main.rb is running.

In my environment, it feels good that the CPU fan doesn't groan even if I run it for a while. (The body gets hot)

Draw a rectangle

I worked in the directory where I unzipped the zip earlier, but I will try working in another location. (To make git management easier)

$ mkdir -p hello-dragonruby-gtk/mygame/app
$ cd hello-dragonruby-gtk/

$ cp ~/Downloads/dragonruby-macos/dragonruby .
$ cp ~/Downloads/dragonruby-macos/font.ttf .

I felt that the dragonruby command was not supposed to be called from another location by setting the PATH, so I copied it. I also needed font.ttf. (Approximately 5.9MB in total)

When you execute it, various directories and files will be created, so gitignore them together.

.gitignore


# DragonRuby
/dragonruby
/font.ttf
/logs
/tmp
/exceptions
console_history.txt

Now that we're ready, let's write the source code to draw the rectangle.

mygame/app/main.rb


def tick args
  args.outputs.solids << [args.grid.center_x - 32, args.grid.h * 0.1, 64, 64]
end

I will do it.

./dragonruby
Screen Shot 2020-06-14 at 23.07.44.png

I was able to draw.

Add it to ʻargs.outputs.solids to draw the shape. Also, since ʻargs.grid contains information such as screen size, I am using that.

It's a little unique, but I don't think it's particularly difficult.

The basic specifications seem to be as follows.

--Screen is 1280x720 pixels --When you change the screen size, it is automatically resized while maintaining the aspect ratio. --The lower left is the origin --FPS is fixed at 60 --The tick I wrote earlier is called frame by frame.

The problem that it is hard to remember the order of the array

Officially, I feel like I'm pushing this notation, but to be honest, it feels painful to kill me for the first time ...

Other writing styles were also prepared properly.

hash

If this is the case, it seems okay to read it for the first time.

mygame/app/main.rb


  args.outputs.solids << { x: args.grid.center_x - 32, y: args.grid.h * 0.1, w: 64, h: 64 }

class

It seems OK to define the following class.

--Return types such as : solid with the primitive_marker method --Has the same attributes as the hash notation key

I'm using this method for now.

mygame/app/primitives.rb


class Primitive
  def initialize(attributes)
    attr_keys.each { |key| send("#{key}=", attributes[key]) }
  end

  def primitive_marker
    self.class.name.downcase
  end

  def attr_keys
    self.class.class_variable_get(:@@attr_keys)
  end

  def serialize
    attr_keys.map { |key| [key, send(key)] }.to_h
  end

  def inspect
    serialize.to_s
  end

  def to_s
    serialize.to_s
  end
end

class Solid < Primitive
  @@attr_keys = %i[x y w h r g b a]
  attr_accessor(*@@attr_keys)
end

If you do not define serialize, ʻinspect, to_s, the console will be filled with the warning when an error occurs, so it is defined. (I have @@ attr_keys` for that ...)

Then load this from main.rb.

mygame/app/main.rb


require 'app/primitives.rb'

def tick args
  args.outputs.solids << Solid.new(x: args.grid.center_x - 32, y: args.grid.h * 0.1, w: 64, h: 64)
end

Draw a line

As an example other than Solid, let's use Line to draw a crosshair in the middle. It is drawn by adding it to ʻargs.outputs.lines`.

I put ʻoutputs and grid in variables because it is troublesome to hit ʻargs every time.

mygame/app/primitives.rb


class Line < Primitive
  @@attr_keys = %i[x y x2 y2 r g b a]
  attr_accessor(*@@attr_keys)
end

mygame/app/main.rb


require 'app/primitives.rb'

def tick args
  outputs, grid = args.outputs, args.grid

  outputs.solids << Solid.new(x: grid.center_x - 32, y: grid.h * 0.1, w: 64, h: 64)

  outputs.lines << Line.new(x: 0, y: grid.center_y, x2: grid.w, y2: grid.center_y)
  outputs.lines << Line.new(x: grid.center_x, y: 0, x2: grid.center_x, y2: grid.h)
end
Screen Shot 2020-06-14 at 23.53.59.png

It was confirmed that the square could be drawn in the center of the left and right.

Games are also classified

Before the tick method gets complicated, classify the game itself.

mygame/app/main.rb


require 'app/primitives.rb'

class Game
  attr_accessor :state, :outputs, :grid

  def tick
    output
  end

  def output
    outputs.solids << Solid.new(x: grid.center_x - 32, y: grid.h * 0.1, w: 64, h: 64)

    outputs.lines << Line.new(x: 0, y: grid.center_y, x2: grid.w, y2: grid.center_y)
    outputs.lines << Line.new(x: grid.center_x, y: 0, x2: grid.center_x, y2: grid.h)
  end

  def serialize
    {}
  end

  def inspect
    serialize.to_s
  end

  def to_s
    serialize.to_s
  end
end
 
$game = Game.new

def tick args
  $game.state = args.state
  $game.outputs = args.outputs
  $game.grid = args.grid
  $game.tick
end

There is a helper called ʻattr_gtk, but I used ʻattr_accessor here because I wanted to access state etc. directly.

Move with keyboard

Use the left and right cursor keys to move the square (player).

The information you want to keep across frames is stored in the state. Keyboard input can be detected by ʻinputs.keyboard.key name. (It seems that you can pick up the hold by ʻinputs.keyboard.key_held.key name, but I don't use it)

The following changes have been made to main.rb.

mygame/app/main.rb


 require 'app/primitives.rb'
 
 class Game
-  attr_accessor :state, :outputs, :grid
+  attr_accessor :state, :outputs, :grid, :inputs
 
   def tick
+    set_defaults
+    handle_inputs
+    update_state
     output
   end
 
+  def set_defaults
+    state.player_x ||= grid.center_x - 32
+    state.player_dx ||= 0
+  end
+
+  def handle_inputs
+    if inputs.keyboard.right
+      state.player_dx = 5
+    elsif inputs.keyboard.left
+      state.player_dx = -5
+    else
+      state.player_dx = 0
+    end
+  end
+
+  def update_state
+    state.player_x += state.player_dx
+  end
+
   def output
-    outputs.solids << Solid.new(x: grid.center_x - 32, y: grid.h * 0.1, w: 64, h: 64)
+    outputs.solids << Solid.new(x: state.player_x, y: grid.h * 0.1, w: 64, h: 64)
 
     outputs.lines << Line.new(x: 0, y: grid.center_y, x2: grid.w, y2: grid.center_y)
     outputs.lines << Line.new(x: grid.center_x, y: 0, x2: grid.center_x, y2: grid.h)
@@ -26,12 +50,13 @@ class Game
     serialize.to_s
   end
 end

 $game = Game.new
 
 def tick args
   $game.state = args.state
   $game.outputs = args.outputs
   $game.grid = args.grid
+  $game.inputs = args.inputs
   $game.tick
 end

Now you can move the rectangle.

move.gif

Make an enemy

Add a gray Solid that moves from side to side. I haven't used any new elements.

mygame/app/main.rb


   def set_defaults
     state.player_x ||= grid.center_x - 32
     state.player_dx ||= 0
+
+    state.enemy_x ||= grid.center_x - 32
+    state.enemy_dx ||= 5
   end
 
   def update_state
     state.player_x += state.player_dx
+
+    state.enemy_x += state.enemy_dx
+    if state.enemy_x < 0 || state.enemy_x > grid.w - 64
+      state.enemy_dx *= -1
+    end
   end
 
   def output
     outputs.solids << Solid.new(x: state.player_x, y: grid.h * 0.1, w: 64, h: 64)
+ 
+    outputs.solids << Solid.new(x: state.enemy_x, y: grid.h * 0.7, w: 64, h: 64, r: 150, g: 150, b: 150)

     outputs.lines << Line.new(x: 0, y: grid.center_y, x2: grid.w, y2: grid.center_y)
     outputs.lines << Line.new(x: grid.center_x, y: 0, x2: grid.center_x, y2: grid.h)
   end

enemy.gif

Shoot bullets

You can add an entity with state.new_entity. It is a way to give information to an entity and use that information to display it on the screen with Solid etc. (I think it can be done without an entity, but since it is prepared, I will use it.)

I use key_up to prevent it from being shot by holding it down.

Also, when the bullet reaches the edge of the screen, it is flagged as dead and deleted. (If there is a dead flag, it will be reject)

mygame/app/main.rb


   def set_defaults
     state.player_x ||= grid.center_x - 32
     state.player_dx ||= 0
 
     state.enemy_x ||= grid.center_x - 32
     state.enemy_dx ||= 5
+
+    state.bullets ||= []
   end
 
   def handle_inputs
     if inputs.keyboard.right
       state.player_dx = 5
     elsif inputs.keyboard.left
       state.player_dx = -5
     else
       state.player_dx = 0
     end
+
+    if inputs.keyboard.key_up.space
+      state.bullets << state.new_entity(:bullet) do |bullet|
+        bullet.y = player_rect[:y]
+        bullet.x = player_rect[:x] + 16
+        bullet.size = 32
+        bullet.dy = 10
+        bullet.solid = { x: bullet.x, y: bullet.y, w: bullet.size, h: bullet.size, r: 255, g: 100, b: 100 }
+      end
+    end
   end

   def update_state
     state.player_x += state.player_dx
 
     state.enemy_x += state.enemy_dx
     if state.enemy_x < 0 || state.enemy_x > grid.w - 64
       state.enemy_dx *= -1
     end
+
+    state.bullets.each do |bullet|
+      bullet.y += bullet.dy
+      bullet.solid[:y] = bullet.y
+
+      if bullet.y > grid.h
+        bullet.dead = true
+      end
+    end
+    state.bullets = state.bullets.reject(&:dead)
   end

   def output
-    outputs.solids << Solid.new(x: state.player_x, y: grid.h * 0.1, w: 64, h: 64)
+    outputs.solids << Solid.new(player_rect)
 
     outputs.solids << Solid.new(x: state.enemy_x, y: grid.h * 0.7, w: 64, h: 64, r: 150, g: 150, b: 150)
+ 
+    outputs.solids << state.bullets.map(&:solid)

     outputs.lines << Line.new(x: 0, y: grid.center_y, x2: grid.w, y2: grid.center_y)
     outputs.lines << Line.new(x: grid.center_x, y: 0, x2: grid.center_x, y2: grid.h)
   end
+
+  private
+
+  def player_rect
+    { x: state.player_x, y: grid.h * 0.1, w: 64, h: 64 }
+  end

bullet.gif

You can now shoot bullets.

Erase the bullet (if it hits the enemy)

You can use ʻintersect_rect` for collision detection.

mygame/app/main.rb


   def update_state
     state.player_x += state.player_dx
 
     state.enemy_x += state.enemy_dx
     if state.enemy_x < 0 || state.enemy_x > grid.w - 64
       state.enemy_dx *= -1
     end
 
     state.bullets.each do |bullet|
       bullet.y += bullet.dy
       bullet.solid[:y] = bullet.y
 
       if bullet.y > grid.h
         bullet.dead = true
       end
+      if bullet.solid.intersect_rect?(enemy_rect)
+        bullet.dead = true
+      end
     end
     state.bullets = state.bullets.reject(&:dead)
   end
 
   def output
     outputs.solids << Solid.new(player_rect)
 
-    outputs.solids << Solid.new(x: state.enemy_x, y: grid.h * 0.7, w: 64, h: 64, r: 150, g: 150, b: 150)
+    outputs.solids << Solid.new(enemy_rect.merge(r: 150, g: 150, b: 150))
 
     outputs.solids << state.bullets.map(&:solid)
 
     outputs.lines << Line.new(x: 0, y: grid.center_y, x2: grid.w, y2: grid.center_y)
     outputs.lines << Line.new(x: grid.center_x, y: 0, x2: grid.center_x, y2: grid.h)
   end
+
+  def enemy_rect
+    { x: state.enemy_x, y: grid.h * 0.7, w: 64, h: 64 }
+  end

collision.gif

It's a little confusing, but when you hit an enemy, the bullet disappears.

Score

If the bullet hits the enemy, it will be +2 points, if it does not hit, it will be -1 point, and the total will be displayed in the upper right.

Use Label to display the text. I used Press Start 2P --Google Fonts to make a dot-like font. (Download and unpack it and place it in mygame / fonts

mygame/app/primitives.rb


class Label < Primitive
  @@attr_keys = %i[x y text size_enum alignment_enum font r g b a]
  attr_accessor(*@@attr_keys)
end

mygame/app/main.rb


   def set_defaults
     state.player_x ||= grid.center_x - 32
     state.player_dx ||= 0
 
     state.enemy_x ||= grid.center_x - 32
     state.enemy_dx ||= 5
 
     state.bullets ||= []
+
+    state.score ||= 0
   end
  
   def update_state
     state.player_x += state.player_dx
 
     state.enemy_x += state.enemy_dx
     if state.enemy_x < 0 || state.enemy_x > grid.w - 64
       state.enemy_dx *= -1
     end
 
     state.bullets.each do |bullet|
       bullet.y += bullet.dy
       bullet.solid[:y] = bullet.y
 
       if bullet.y > grid.h
         bullet.dead = true
+        state.score -= 1
       end
       if bullet.solid.intersect_rect?(enemy_rect)
         bullet.dead = true
+        state.score += 2
       end
     end
     state.bullets = state.bullets.reject(&:dead)
   end
 
   def output
     outputs.solids << Solid.new(player_rect)
 
     outputs.solids << Solid.new(enemy_rect.merge(r: 150, g: 150, b: 150))
 
     outputs.solids << state.bullets.map(&:solid)
+
+    outputs.labels << Label.new(
+      x: grid.w * 0.99,
+      y: grid.h * 0.98,
+      text: state.score,
+      alignment_enum: 2,
+      font: 'fonts/Press_Start_2P/PressStart2P-Regular.ttf'
+    )
 
     outputs.lines << Line.new(x: 0, y: grid.center_y, x2: grid.w, y2: grid.center_y)
     outputs.lines << Line.new(x: grid.center_x, y: 0, x2: grid.center_x, y2: grid.h)
   end

score.gif

Now that I have a score, I can play (for the time being).

Use images

I'm lonely if it stays square, so I'll use an image. (The image is the one I drew before, so the quality is that ...)

Place player.png, ʻenemy.png, and bullet.pnginmygame / sprites`.

After that, change the code to use Sprite instead of Solid. I don't need the crosshairs anymore, so I'll erase them.

mygame/app/primitives.rb


class Sprite < Primitive
  @@attr_keys = %i[
    x y w h path angle a r g b
    source_x source_y source_w source_h
    flip_horizontally flip_vertically
    angle_anchor_x angle_anchor_y
    tile_x tile_y tile_w tile_h
  ]
  attr_accessor(*@@attr_keys)
end

mygame/app/main.rb


   def handle_inputs
     if inputs.keyboard.right
       state.player_dx = 5
     elsif inputs.keyboard.left
       state.player_dx = -5
     else
       state.player_dx = 0
     end
 
     if inputs.keyboard.key_up.space
       state.bullets << state.new_entity(:bullet) do |bullet|
         bullet.y = player_rect[:y]
         bullet.x = player_rect[:x] + 16
         bullet.size = 32
         bullet.dy = 10
-        bullet.solid = { x: bullet.x, y: bullet.y, w: bullet.size, h: bullet.size, r: 255, g: 100, b: 100 }
+        bullet.sprite = { x: bullet.x, y: bullet.y, w: bullet.size, h: bullet.size, r: 255, g: 100, b: 100, path: 'sprites/bullet.png' }
       end
     end
   end
 
   def update_state
     state.player_x += state.player_dx
 
     state.enemy_x += state.enemy_dx
     if state.enemy_x < 0 || state.enemy_x > grid.w - 64
       state.enemy_dx *= -1
     end
 
     state.bullets.each do |bullet|
       bullet.y += bullet.dy
-      bullet.solid[:y] = bullet.y
+      bullet.sprite[:y] = bullet.y
 
       if bullet.y > grid.h
         bullet.dead = true
         state.score -= 1
       end
-      if bullet.solid.intersect_rect?(enemy_rect)
+      if bullet.sprite.intersect_rect?(enemy_rect)
         bullet.dead = true
         state.score += 2
       end
     end
     state.bullets = state.bullets.reject(&:dead)
   end
 
   def output
-    outputs.solids << Solid.new(player_rect)
+    outputs.sprites << Sprite.new(player_rect.merge(path: 'sprites/player.png'))
 
-    outputs.solids << Solid.new(enemy_rect.merge(r: 150, g: 150, b: 150))
+    outputs.sprites << Sprite.new(enemy_rect.merge(r: 150, g: 150, b: 150, path: 'sprites/enemy.png'))
 
-    outputs.solids << state.bullets.map(&:solid)
+    outputs.sprites << state.bullets.map(&:sprite)
 
     outputs.labels << Label.new(
       x: grid.w * 0.99,
       y: grid.h * 0.98,
       text: state.score,
       alignment_enum: 2,
       font: 'fonts/Press_Start_2P/PressStart2P-Regular.ttf'
     )
-    outputs.lines << Line.new(x: 0, y: grid.center_y, x2: grid.w, y2: grid.center_y)
-    outputs.lines << Line.new(x: grid.center_x, y: 0, x2: grid.center_x, y2: grid.h)     
   end

sprite.gif

Compared to the beginning, it looks a lot like that!

Packaging

Prepare the metadata. For icon, copy player.png.

mygame/metadata/game_metadata.txt


devid=hello-dragonruby-gtk
devtitle=Hello DragonRuby GTK
gameid=hello-dragonruby-gtk
gametitle=Hello DragonRuby GTK
version=0.1
icon=metadata/icon.png

I copied dragonruby-publish` but it didn't work for some reason, so Do it in the directory where you downloaded and unzipped DragonRuby. (Refer to the postscript below for how to copy and move)

$ cp mygame/sprites/player.png mygame/metadata/icon.png
$ cp -r mygame/ ~/Downloads/dragonruby-macos/hello-dragonruby-gtk
$ cd ~/Downloads/dragonruby-macos
$ ./dragonruby-publish --only-package hello-dragonruby-gtk

This will output the HTML5 version along with the files for other platforms. (builds/hello-dragonruby-gtk-html5-0.1

If you publish this, you can play it in your browser. It's a big deal, so I put it on GitHub Pages.

https://tnantoka.github.io/hello-dragonruby-gtk/

Copy and move dragonruby-publish (additional note)

It worked when I copied various things other than dragonruby-publish.

$ cp ~/Downloads/dragonruby-macos/dragonruby-publish .
$ cp ~/Downloads/dragonruby-macos/.dragonruby .
$ cp ~/Downloads/dragonruby-macos/*.png .
$ cp ~/Downloads/dragonruby-macos/open-source-licenses.txt .

$ ./dragonruby-publish --only-package
```

 There are more things to do `.gitignore`.


#### **`.gitignore`**
```python

# DragonRuby
/dragonruby*
/font.ttf
/logs
/tmp
/exceptions
/console_history.txt
/.dragonruby
/builds
/console-logo.png
/open-source-licenses.txt
```

# Source code

 It is published below.
 (DragonRuby is required separately)

https://github.com/tnantoka/hello-dragonruby-gtk/

# Impressions

 I was able to play without any big addiction.

 Can it be used in Production? I don't know what it is, but
 It seemed to be usable enough to make a little thing.

 I wanted to do creative coding with Ruby, which I'm used to writing, so can I use it as a tool? I think that.
 (For that purpose, I would like to see the physics engine installed and the drawing enhanced.)

 I will try to find time and touch it again.

# References

 There is no API reference (?), So I searched around while looking around.

- README.md
 ――Read here first
- CHEATSHEET.md
 ――Read this too
- [DragonRuby Game Toolkit by DragonRuby](https://dragonruby.itch.io/dragonruby-gtk)
 --There is a description such as Entity that is not in the README
- mygame/documantion
 --Solid, Sprite, etc. will explain each element as it is
- samples
 --There are 67 samples (MIT license!)



Recommended Posts

I played with DragonRuby GTK (Game Toolkit)
I played with wordcloud!
Simple typing game with DragonRuby
I played with PyQt5 and Python3
I played with Mecab (morphological analysis)!
[Scikit-learn] I played with the ROC curve
[Introduction to Pytorch] I played with sinGAN ♬
I made a life game with Numpy
I made a roguelike game with Python
[Python] I introduced Word2Vec and played with it.
I want to make a game with Python
[Python] I played with natural language processing ~ transformers ~
I played with Floydhub for the time being
I played with Diamond, a metrics collection tool
I made a bin picking game with Python
I made a Christmas tree lighting game with Python
I made a vim learning game "PacVim" with Go
I made a falling block game with Sense HAT
〇✕ I made a game
[Python] I installed the game from pip and played it
I made a puzzle game (like) with Tkinter in Python
I tried a stochastic simulation of a bingo game with Python