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.
asked May 17, 2013 at 1:07
\$\endgroup\$

1 Answer 1

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.

answered May 17, 2013 at 14:27
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.