Z-ordering sprites

BlitzMax Forums/BlitzMax Beginners Area/Z-ordering sprites

Adam Novagen(Posted 2012) [#1]
At the moment, I'm collecting my thoughts on a kind of universal 2D sprite system, as I mentioned in this thread. After reading about delta timing and timesteps in this article, however, I've made a few mental alterations to this scheme to allow my rendering to be reduced to one simple, self-contained function, like this:

Type sprite
    Global list:TList
    
    Field x:Float,y:Float ' Coords
    Field w:Float,h:Float ' Width, Height
    Field r:Float ' Rotation
    Field a:Float ' Alpha
    Field image:Int,f:Int ' Image, Frame
    
    Method draw()
        SetTransform r,w,h
        SetAlpha a
        DrawImage image,x,y,f
    EndMethod
    
    Function render()
        For Local s:sprite = EachIn sprite.list
            s.draw()
        Next
    End Function
EndType

The idea here - as yet untested - is that all logic is taken care of according to the timesteps, and then sprite.render() is called once to draw every single sprite on the screen, in a single pass. (This example currently disregards delta timing for the sake of simplicity.) This whole concept actually seems very similar to Blitz3D's entity system, where everything "exists" for as long as it's created, but is only drawn if it's onscreen and of course visible (ShowEntity() and HideEntity()).

Obviously, this brings one potential problem to the surface: drawing order. As it stands right now, all the sprites will be drawn in the order they were created in, and as objects come and go that of course will become an issue. The solution that comes to my mind is to add a z-order field:

Field z:Int

Fine and dandy until it comes time to actually sort the objects. I know that lists and arrays can be sorted, but is there a way to sort a list of objects by one of the objects' fields? If not, how would I go about z-sorting these sprites?


Yasha(Posted 2012) [#2]
is there a way to sort a list of objects by one of the objects' fields?


If you don't supply a comparison function to TList.Sort, it will use the .Compare method of the objects in the list (.Compare is a method of the Object root class so it can be called on anything; the default implementation is not very useful).

So all you have to do is override the .Compare method in sprite with one that compares them based on their z field, and use Sort on the list, passing nothing as the second argument. Or alternatively if that's not what you want to be the generic comparison behaviour for all sprites in all contexts, just implement a function (not a method) that compares two sprites, and pass that in as the second argument to Sort. Comparison functions' behaviour (or Compare methods') should be as follows:

Returns a value less than 0 if an object is less than another object, a value greater than 0 if an object is greater than another object or the value 0 if an object equals another object.

(Help Index -> Language -> Objects)


(Never used a function pointer before? Then I recommend building the specific-case solution. Sorting a list is probably the most immediately straightforward example of why you would want a function pointer and what a higher-order function does, which can be difficult if coming from Blitz Classic.)

Last edited 2012


Adam Novagen(Posted 2012) [#3]
Okay okay, wrapping my brain a round this... I looked up function pointers and found a very basic C example on Wikipedia, which combined with some recent C++ code I was looking at that used function pointers (though I didn't know it until now), allowed me to understand the concept at least vaguely.

I've got this far:

Type thing
	Global list:TList
	
	Field id:Int
	Field z:Int
	
	Method compare()
	End Function
	
	Function make()
		Local t:thing = New thing
			t.id = i
			t.z = Rand(100)
			
			list.addLast t
	EndFunction
	
	Function zSort()
		list.sort True
	End Function
	
EndType

When I reached this point though, I hit a snag. If my code creates, say, ten sprite objects, then how do I decide WHICH two sprites to check in .compare()?


Yasha(Posted 2012) [#4]
Compare accepts one object as parameter and compares it to Self (i.e. it's a method). So for its purposes it's completely generic: just a comparison between any two objects.

TList.Sort applies Compare to items in the list within its Sort method, in such a fashion that it can generate enough information to completely sort it (look at the TList source to find out exactly what it does). Which objects actually get compared is the list's business, not yours. Your role is only to tell it that it may compare "whatever is necessary" for it to get the job of sorting done. If you're using the list's own Sort method, you don't need to call Compare yourself at any point. You only need to define the actions involved in one comparison operation.

This is part of the business of separating concerns. TList doesn't need to know that there is such a type as sprite, only that whatever it receives can be sorted in one way or another (it knows this because all objects inherit the basic comparison method from Object; or because you provided a function that implements that comparison mechanic). sprite meanwhile doesn't need to know how TLists are sorted, because that's an implementation detail of lists (and could be subject to change if BRL developed a more efficient sorting algorithm), only how to define an ordering relationship between any one sprite and any other sprite. Therefore the business of which sprites get compared is not one of the sprite class's concerns.

Last edited 2012


Adam Novagen(Posted 2012) [#5]
OHHH, oh oh oh. Okay. I'm finally starting to understand the benefits of having knowledge and work removed from my hands. I was able to get this out:

	Method compare:Int(t:thing)
		If Self.z < t.z Then Return -1
		If Self.z > t.z Then Return 1
		' z = z is the only remaining possibility, and as it returns 0 it would be redundant.
	EndMethod

Now, however, I'm getting the error "Overriding method differs by type." I assumed it should be type Int since it returns 1, -1 or 0... What type SHOULD this method be?

Last edited 2012


Midimaster(Posted 2012) [#6]
SuperStrict

Type thing
	Global list:TList=New TList
	Global i%
	
	Field id:Int
	Field z:Int
	
	Method compare:Int(OtherThing:Object)
		If z<thing(OtherThing).z Return 1
		If z>thing(OtherThing).z Return -1
		Return 0
	End Method
	
	Function make()
		Local t:thing = New thing
			t.id = i
			i=i+1
			t.z = Rand(100)
			list.addLast t
	EndFunction
	
	Function zSort()
		list.sort True
	End Function
	
EndType


For Local i%=0 To 99
 thing.Make
Next

thing.list.sort()

For Local t:thing = EachIn thing.list
 Print t.z
Next


[EDIT]
upps.... i see you got it meanswhile by yourself....

the return type is INTEGER, the problem must be anywhere else... Perhaps you MUST return 0 if z=z

try this:

	Method compare:Int(OtherThing:Object)
		If z<thing(OtherThing).z Return 1
		If z>thing(OtherThing).z Return -1
		Return 0
	End Method



Last edited 2012


Yasha(Posted 2012) [#7]
The parameter to Compare needs to be an Object, and you can then cast it to the right type yourself (casting doubles as typechecking: if the type is wrong you'll get back a Null) within the method body. This is because the signature of the topmost definition of Compare was Int(Object), so yours must be identical to override it.

None of them tasty overloaded methods or generics you got used to in Java, sorry.


Adam Novagen(Posted 2012) [#8]
EDIT: nvm, tinkering.

Last edited 2012


Kryzon(Posted 2012) [#9]
Why is it necessary to cast Object into something else? (that is, what is the "technical" reason behind it?)

Last edited 2012


Midimaster(Posted 2012) [#10]
changes my code sample in #6 into a runable one ;-)


Adam Novagen(Posted 2012) [#11]
Ergh. As much as I hate sounding like Thundros, trying to get everyone else to write my code for me, I'm really flying completely blind here. How am I to cast an object into... Hell I only half-understood the explanation anyway, sorry. I tried this as an educated guess:

	Method compare:Int(thingIn:Object)
		Local t:thing = thingIn
		If Self.z < t.z Then Return -1
		If Self.z > t.z Then Return 1
		' z = z is the only remaining possibility, and as it returns 0 it would be redundant.
	EndMethod

This just landed me with an "identifier 't' not found" compile error though, so now I'm completely lost.


Adam Novagen(Posted 2012) [#12]
WAIT

hold on

wait.

Wait. I think I may have this. Was missing something obvious, hold the phone a tick.


Kryzon(Posted 2012) [#13]
MidiMaster has code in #6 doing the casting, but for the anonymous reader, casting is done like this:

NewType( UnknownType )

So to cast a String to Integer: Int( String )

To cast 'thingIn:Object' to 'thing' (I really think you should change it to 'Tsprite' or something...), do this:

thing( thingIn )


Adam Novagen(Posted 2012) [#14]
OKAY GOT IT.


Thanks to everyone here, including Midimaster's alterations, I now have this working. I feel great, thanks for the help. Now before the thread closes, would anyone mind explaining to me what exactly the whole object-casting business is/was about, and what exactly thing(t).z is doing?

EDIT: @Kryzon Yeah, the actual framework uses sprite as the class name, thing is just for the sake of the example, like foo that Yasha's so fond of ;D

Last edited 2012


Yasha(Posted 2012) [#15]
First: every type named T has an associated cast operator used T(something). So when you see a type name used this way, it's the cast operation happening.

Casting is to do with type conversion. There are two main uses, one trivial and the other not so much.

First, the easy one: with "primitive" types, you can use it to convert one to another:

Local f:Float = 5.5
Print Int(F)



For the more complicated use, you need to be comfortable with the notion of inheritance. When one type extends another, any instances of the extending type are also legitimate instances of the super type: they contain all of its fields and, their methods implement the same interface, so anywhere an instance of the super type could be used, an instance of the child type can be used also. i.e. OOP introduces the idea that one value may have more than one legitimate type (in the sense of "use context") in code. BlitzMax uses this as the basis for its implementation of polymorphism (the super type defines an interface, child types provide different implementations of the methods, multiple instances of the super type may therefore pack different behaviours for the same method selectors).

So while you can "upcast" implicitly - just assign a value of a child type to a variable of a super type, and there is no problem because we know the usage will be valid - "downcasting" (assignment of a value of super type to a variable of child type) requires an actual check to see if the new usage you're asking for is legitimate: it could be that the value is of a different child type; we don't know from its current usage.

All the cast operation does, then, is check whether the value given to it can be used in the context of the requested type: if so, it returns it, but the returned value is in the new context that the compiler recognises lets it be used as the requested type. If not, it returns nothing and expects you to handle this condition somehow (or to catch the ensuing null reference exception somewhere). The fact it returns nothing means you might sometimes see "If someType(someThing)" as a type check condition (don't use this if you can use polymorphism instead).

Note that also because it returns nothing on failure, the idiom "T(obj).meth()" is not safe unless you wrap your code in exception handlers, or you can prove at compile-time that it will never receive an object of the wrong type (i.e. that it's never exposed to an untrusted source of input) and is just being used to work around the type system. So don't use this in your Compare method unless it's never called from anywhere you can't predict.

(Note that because of this rather limited type system with no generics, BlitzMax lets you do things like build fully-polymorphic lists of completely unrelated objects. If someone made a list of sprites and also ...IDK, sounds... this would be entirely permitted; calling Sort on this list would result in some failed casts.)


Object could be anything, up to and including strings, arrays and streams. There is no guarantee that the second object being passed to Compare actually is another sprite, because the signature of the method allows any old thing to be passed in (having been set as Object by Object's default version). So you need to confirm that you're not trying to compare a sprite to an incoming network connection, before you go ahead and actually do it.

Last edited 2012


Adam Novagen(Posted 2012) [#16]
I see; this really does make sense. Some bits of how this relates to syntax are still a mite fuzzy, but I'll pick that up with time; at least now I truly understand how the casting is going, for example the fact that all classes are automatically an extension or child of the generic Object. So for instance, if I have something like this:

Type sprite
'...
EndType

Type ball Extends sprite
'...
endType


Any instances of ball will essentially be Object -> sprite -> ball, and therefore if I have a ball then I automatically know it's a sprite and an Object. However, if I have an Object, then it may or may not be a sprite, which in turn may or may not be a ball. Am I following?


Kryzon(Posted 2012) [#17]
Great info, thanks guys.


*(Posted 2012) [#18]
couldnt you have a render list and put the sprites in order into that then render?


Adam Novagen(Posted 2012) [#19]
Yes, but you need to think about the kind of performance hit that would mean. It would require scanning [up to] the entire list of sprite objects to find the next sprite in the z-order sequence multiple times, until every sprite had been added to the z-ordered list. It's a brute force solution, and brute force is hardly ever a viable option any more.


TaskMaster(Posted 2012) [#20]
Wouldn't it be easier to just keep them in order as they move?

Every time one moves, check to see if the Y axis (or whatever) is now higher or lower than its neighbor in the ordered list.


Adam Novagen(Posted 2012) [#21]
A background is a sprite.

A scoreboard is a sprite.

A scoreboard's TEXT can be a sprite.

A playing board can be a sprite (or sprites).

The playing pieces are sprites.

Particle effects are sprites.

Vertical orientation is nowhere near enough to get an accurate sense of whether a sprite should be drawn in front of or behind other sprites. Also remember that its neighbours in the list are not necessarily the nearest sprites onscreen, and in fact usually won't be. Even if vertical orientation was enough to go on, you'd still have to sort the list by Y instead of Z.

I get where you're coming from, and it's true that I could simple use procedural concepts like:

Function render()
    drawBackground()
    drawGameBoard()
    drawTokens()
    drawFrags()
EndFunction
This would certainly WORK, but it's mediocre design; the function render() would have to be re-written according to each game it was used in, and therefore it would neither be modular nor universal. What if I made a game that had no game board? What if I had multiple types of frags/particles? What if some tokens had to be draw earlier in the loop, and others drawn later? By allowing z-sorting, I'm free of needing to worry about any of that beforehand:

Function render()
    sprite.zSort()
    For s:sprite = EachIn sprite.list
        s.draw()
    Next
EndFunction
Clean, quick, and simple. The framework of any 2D game I make with this can be exactly the same; I never have to worry about the code inside render(), all I need to know is that I call it once per frame to draw everything correctly on screen. This is the nature of modular code: once a method or function has been completed, you should only ever have to use it, not alter or tailor it.

Last edited 2012