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 newSprite
andLabel
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.
1 Answer 1
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 return
s. 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.