2
\$\begingroup\$

I am creating a game, with Ruby scripting.

Sprite and Label objects can be drawn on the screen. Depending on the order you draw them, one will overlap the other.

To make things easier, I want to implement a property in my drawable objects: Z order. So the higher the Z order of one object, the closer it will be to the player so to speak.

For this, I decided to create a class that will be in charge of containing my Sprite and Label objects, and depending on their Z property, draw them in order.

When a Sprite or Label object is created, they are automatically assigned to my global Renderer class.

Here is my Renderer class:

class Renderer
  EXPECTED_CLASSES = [Sprite,Label]

  # ----------------------------------------------------------------------------------------------------
  # * Constructor
  # ----------------------------------------------------------------------------------------------------
  def initialize
    @sprite_batch = Sprite_Batch.new
    @stack = {}
    @assigned_items = []
    @sorted_keys = []
  end

  # ----------------------------------------------------------------------------------------------------
  # * Add a drawable element with a Z order value
  # ----------------------------------------------------------------------------------------------------
  def add(item,z = 0)
    if (assignable?(item))
      @assigned_items.push(item)
      if (@stack.has_key?(z))
        @stack[z].push(item)
      else
        @stack[z] = [item]
      end
      @sorted_keys = @stack.keys.sort
    end
  end

  # ----------------------------------------------------------------------------------------------------
  # * Remove an item from this manager
  # ----------------------------------------------------------------------------------------------------
  def remove(item)
    if (removable?(item))
      key = item.z
      @stack[key].delete(item)
      @assigned_items.delete(item)
      if (@stack[key].empty?)
        @stack.delete(key)
        @sorted_keys.delete(key)
      end
    end
  end

  # ----------------------------------------------------------------------------------------------------
  # * Reorder item
  # ----------------------------------------------------------------------------------------------------
  def reorder(item,z)
    remove(item)
    add(item,z)
  end

  # ----------------------------------------------------------------------------------------------------
  # * Render items in order
  # ----------------------------------------------------------------------------------------------------
  def render
    @sprite_batch.begin
    for key in @sorted_keys
      for item in @stack[key]
        @sprite_batch.draw(item)
      end
    end
    @sprite_batch.end
  end

  # ----------------------------------------------------------------------------------------------------
  # * Determine if an item can be added
  # ----------------------------------------------------------------------------------------------------
  def assignable?(item)
    if (removable?(item) || !EXPECTED_CLASSES.include?(item.class))
      return false
    end
    return true
  end

  # ----------------------------------------------------------------------------------------------------
  # * Determine if an item can be removed
  # ----------------------------------------------------------------------------------------------------
  def removable?(item)
    if (!@assigned_items.include?(item))
      return false
    end
    return true
  end

end

You probably noticed Sprite_Batch there. Don't pay it attention - it is just where the actual drawing occurs, but it is beyond the purpose of this question.

What I want to know - how can I further improve this class to be able to draw the objects in the right order depending on their Z property?

This class works alright, but I'm afraid it could get a bit slow when I have lots of objects for drawing.

Notes

  • The render method is called whenever the window wants to update its contents. So it is called a lot of times in a short interval.
  • The reorder method is used rarely, so I guess it is fine.
  • The add method is not called very often throughout gameplay, but at startup of a specific scene (like a new game level), a large batch of new Sprite and Label objects are to be expected.
  • I've considered removing the validation when adding/removing items and instead let the game crash if I did a mistake in a script.
\$\endgroup\$
2
\$\begingroup\$

how can I further improve this class to be able to draw the objects in the right order depending on their Z property?

The abstraction for the task is a priority queue, that's it, a dynamic structure that keeps elements sorted by a defined order (here the z-index). A priority queue is efficiently implemented with a heap, which has O(log n) find/insertion/deletion, which is pretty cool, and can be traversed in O(n). A Ruby gem that implements a priority queue: pqueue. Of course, if you have few sprites it won't improve much and it's not worth the hassle, but it's good to know about these algorithms. In your case:

require 'pqueue'
pq = PQueue.new(initial_sprites) { |s1, s2| s1.z <=> s2.z }
pq.push(new_sprite)

You use an imperative approach throughout the code. I won't argue that a game has extremely fast dynamic changes and it may be the only reasonable choice (at least in an imperative language like Ruby), but note that every time that you change some variable in-place, you're making the code harder to understand (because potentially everything can be changed everywhere, functions are no longer black boxes that get things and return things). Try to minimize in-place updates and side-effects. If you are curious: Functional Programming and Ruby.

Instead of:

for key in @sorted_keys
  for item in @stack[key]
    @sprite_batch.draw(item)
  end
end

This is more idiomatic:

@sorted_keys.each do |key|
  @stack[key].each do |item|
    @sprite_batch.draw(item)
  end
end

Instead of:

def assignable?(item)
  if (removable?(item) || !EXPECTED_CLASSES.include?(item.class))
    return false
  end
  return true
end

First, it's not idiomatic to write explicit returns. Second, it's more clear if you write a full if/else, the branches of the conditional (along with its indendation) conveys a lot of information to the reader. Also, you have a boolean already, if boolean then false else true -> !boolean, so the branches are unnecessary. You can simply write:

def assignable?(item)
  !(removable?(item) || !EXPECTED_CLASSES.include?(item.class))
end

Or using De Morgan law, in possitive form:

def assignable?(item)
  !removable?(item) && EXPECTED_CLASSES.include?(item.class)
end

More on idiomatic Ruby.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.