Ruby_for_Dragons

An unofficial companion to the DragonRuby documentation

View on GitHub
Home DragonRuby API Documentation Quick Answers Kota’s Cheatsheet Sitemap

Object Oriented Programming

A short OOP Tutorial.

By Kniknoo

Using Classes in DragonRuby

A common point of confusion for experienced Rubyists coming into DragonRuby is “Can I use Objects instead of global state?”. Absolutely!

This documents aims to help users get started writing their own classes. If you’ve tried this on your own, you may have noticed that the Dragon doth protest and offers some cryptic clues about solutions. There are 3 common gotchas that will be covered before expanding into several distinct approaches to using these classes in DragonRuby.

The first two gotchas come up when trying to use classes to represent the basic building blocks of DragonRuby, the primitives. When the renderer is trying to directly render a class reference, there are two key groups of data that it is always looking for, the attributes and the inspect method.

Attributes

Attributes or Instance Variables are a key component to how an Object talks to the outside world and to itself. The Dragon Renderer will be looking for a certain set of these for each primitive type that correspond to the keys used in hash form.

The most basic and universal of these are

attr_accessor :x, :y, :r, :g, :b, :a, :primitive_marker, :blendmode_enum

x and y are the coordinates of the bottom left corner, r g b and a are the red green blue and alpha components of the color, and a primitive marker tells the args.outputs.primitives array what kind of primitive this class represents. blendmode_enum gets into some more advanced uses of color mixing, but for now it’s only important to know that it’s a required attribute.

In addition to the above universals, each primitive type has its own set of attributes that also need to be set.

Labels:

attr_accessor :text, :size_enum, :alignment_enum, :font

Borders and Solids:

attr_accessor :w, :h

Lines:

attr_accessor :x2, :y2

Sprites:

attr_accessor :w, :h, :path, :angle, :source_x, :source_y, :source_w, :source_h,
              :tile_x, :tile_y, :tile_w, :tile_h, :flip_horizontally,
              :flip_vertically, :angle_anchor_x, :angle_anchor_y

I write it out this way so that you can see exactly which attributes exist with each primitive type. In theory, this works fine, but in practice, this can lead to headaches from breaking changes when new attributes are added. Because of this, there are special attr calls that do the above and evolve with the system. attr_sprite will fill in all of the universals plus the sprite attrs, and attr_rect will fill in the universals plus the Solid and Border attrs. In addition, there is an attr_gtk that will offer access to args using the method signature described at https://github.com/DragonRuby/dragonruby-game-toolkit-contrib/blob/a1b8552ae442d1fa22df1e569652eb99750dcebf/dragon/attr_gtk.rb

All of these are declared like attr_accessor, that is, preferably near the top of your class declaration. Unlike attr_accessor, they take no arguments.

The next thing to know about are three methods that are commonly called on objects by DragonRuby, and will cause errors that obscure your actual error within a class if they aren’t at least present. These are #serialize, #inspect, and #to_s. While you can get away with leaving these defined but empty, you’ll be doing yourself favors in the future by filling these out with useful info. #inspect is a representation that will show up if you add your object to state, and is also displayed when an exception occurs in the terminal. #to_s is called on a number of terminal operations involving the object, such as passing its reference to another variable. #serialize will be displayed in exception logs outside of the terminal.

Require

The final gotcha that will come up is when you inevitably decide to start breaking your program into multiple files. If you’re used to the Ruby way of doing require, you will wind up with errors that don’t seem to make sense, like claims that the class you’ve made doesn’t exist. First off, there is no require relative method. Require will work a lot like require_relative though. Secondly, the relative path from your game folder’s root and the file extension are mandatory.

Instead of

require "my_library"

you’ll want

require "lib/my_library.rb"

Require is done in an asynchronous way, meaning that your tick method in main will already be running while requires are loading data from other files. If you’re expecting a specific order of operations, this can be very confusing. In order to get around this, you can delay the onset of tick be putting it into a file that is required last instead of leaving it in main. My own usual setup is to have main.rb responsible for requiring everything and doing any tasks that I want to be complete before the first tick happens. So for example:

#main.rb

require "lib/my_lib1.rb"
require "lib/my_lib2.rb"
require "lib/my_lib3.rb"
require "app/tick.rb"
#tick.rb

def tick(args)
  my_lib ||= MyLib.new
  #game methods and so on
end

When you require a file, Ruby adds everything inside of it to its memory and treats everything together like one gigantic file. This means, for instance, you might have a constant defined in file_one while being referenced in file_two and this all works as if they were in the same file. By extension of this, if you reopen and redefine a class or method Ruby will only see the last definition of it. So you wouldn’t want to have a my_method in one file that is relied upon by parts of that file and another my_method in another file that does something different. Only one of these would be remembered, and only the most recently evaluated version will be that one. The same goes for classes. If you’re afraid you might accidentally cause a naming collision like this, your best solution is to put your class or method inside of a module and use a fully qualified name.

module MyLib1
  def self.my_method
    "Look I made a method!"
  end
end
module MyLib2
  def self.my_method
    "And here is another method!"
  end
end
MyLib1.my_method
#=> Look I made a method!
MyLib2.my_method
#=> And here is another method!