gamnit: fix animations with a single frame
[nit.git] / lib / gamnit / flat / flat_core.nit
index e45f8d1..800b3de 100644 (file)
@@ -241,6 +241,32 @@ class Sprite
                tint_direct = value
        end
 
+       # Draw order, higher values cause this sprite to be drawn latter
+       #
+       # Change this value to avoid artifacts when drawing non-opaque sprites.
+       # In general, sprites with a non-opaque `texture` and sprites closer to
+       # the camera should have a higher value to be drawn last.
+       #
+       # Sprites sharing a `draw_order` are drawn in the same pass.
+       # The sprite to sprite draw order is undefined and may change when adding
+       # and removing sprites, or changing their attributes.
+       #
+       # ### Warning
+       #
+       # Changing this value may have a negative performance impact if there are
+       # many different `draw_order` values across many sprites.
+       # Sprites sharing some attributes are drawn as group to reduce
+       # the communication overhead between the CPU and GPU,
+       # and changing `draw_order` may break up large groups into smaller groups.
+       var draw_order = 0 is writable(draw_order_direct=)
+
+       # Set draw order,  see `draw_order`
+       fun draw_order=(value: Int)
+       do
+               if isset _draw_order and value != draw_order then needs_remap
+               draw_order_direct = value
+       end
+
        # Is this sprite static and added in bulk?
        #
        # Set to `true` to give a hint to the framework that this sprite won't
@@ -352,10 +378,10 @@ redef class App
        var ui_camera = new UICamera(app.display.as(not null)) is lazy
 
        # World sprites drawn as seen by `world_camera`
-       var sprites: Set[Sprite] = new SpriteSet
+       var sprites = new SpriteSet
 
        # UI sprites drawn as seen by `ui_camera`, over world `sprites`
-       var ui_sprites: Set[Sprite] = new SpriteSet
+       var ui_sprites = new SpriteSet
 
        # Main method to refine in clients to update game logic and `sprites`
        fun update(dt: Float) do end
@@ -511,7 +537,7 @@ redef class App
        # Draw world sprites from `sprites`
        protected fun frame_core_world_sprites(display: GamnitDisplay)
        do
-               frame_core_sprites(display, sprites.as(SpriteSet), world_camera)
+               frame_core_sprites(display, sprites, world_camera)
        end
 
        # Draw UI sprites from `ui_sprites`
@@ -520,7 +546,7 @@ redef class App
                # Reset only the depth buffer
                glClear gl_DEPTH_BUFFER_BIT
 
-               frame_core_sprites(display, ui_sprites.as(SpriteSet), ui_camera)
+               frame_core_sprites(display, ui_sprites, ui_camera)
        end
 end
 
@@ -849,26 +875,26 @@ redef class OffsetPoint3d
 end
 
 # Set of sprites sorting them into different `SpriteContext`
-private class SpriteSet
+class SpriteSet
        super HashSet[Sprite]
 
-       # Map texture then static vs dynamic to a `SpriteContext`
-       var contexts_map = new HashMap3[RootTexture, nullable RootTexture, Bool, Array[SpriteContext]]
-
-       # Contexts in `contexts_map`
-       var contexts_items = new Array[SpriteContext]
-
-       # Sprites needing resorting in `contexts_map`
-       var sprites_to_remap = new Array[Sprite]
-
        # Animation speed multiplier (0.0 to pause, 1.0 for normal speed, etc.)
        var time_mod = 1.0 is writable
 
        # Seconds elapsed since the launch of the program, in world time responding to `time_mod`
        var time = 0.0
 
+       # Map texture then static vs dynamic to a `SpriteContext`
+       private var contexts_map = new HashMap4[RootTexture, nullable RootTexture, Bool, Int, Array[SpriteContext]]
+
+       # Contexts in `contexts_map`, sorted by draw order
+       private var contexts_items = new Array[SpriteContext]
+
+       # Sprites needing resorting in `contexts_map`
+       private var sprites_to_remap = new Array[Sprite]
+
        # Add a sprite to the appropriate context
-       fun map_sprite(sprite: Sprite)
+       private fun map_sprite(sprite: Sprite)
        do
                assert sprite.context == null else print_error "Sprite {sprite} belongs to another SpriteSet"
 
@@ -877,7 +903,8 @@ private class SpriteSet
                var animation = sprite.animation
                var animation_texture = if animation != null then
                        animation.frames.first.root else null
-               var contexts = contexts_map[texture, animation_texture, sprite.static]
+               var draw_order = sprite.draw_order
+               var contexts = contexts_map[texture, animation_texture, sprite.static, draw_order]
 
                var context = null
                if contexts != null then
@@ -892,15 +919,17 @@ private class SpriteSet
 
                if context == null then
                        var usage = if sprite.static then gl_STATIC_DRAW else gl_DYNAMIC_DRAW
-                       context = new SpriteContext(texture, animation_texture, usage)
+                       context = new SpriteContext(texture, animation_texture, usage, draw_order)
 
                        if contexts == null then
                                contexts = new Array[SpriteContext]
-                               contexts_map[texture, animation_texture, sprite.static] = contexts
+                               contexts_map[texture, animation_texture, sprite.static, draw_order] = contexts
                        end
 
                        contexts.add context
+
                        contexts_items.add context
+                       sprite_draw_order_sorter.sort(contexts_items)
                end
 
                context.sprites.add sprite
@@ -917,7 +946,7 @@ private class SpriteSet
        end
 
        # Remove a sprite from its context
-       fun unmap_sprite(sprite: Sprite)
+       private fun unmap_sprite(sprite: Sprite)
        do
                var context = sprite.context
                assert context != null
@@ -928,14 +957,16 @@ private class SpriteSet
        end
 
        # Draw all sprites by all contexts
-       fun draw
+       private fun draw
        do
+               # Remap sprites that may need to change context
                for sprite in sprites_to_remap do
                        unmap_sprite sprite
                        map_sprite sprite
                end
                sprites_to_remap.clear
 
+               # Sort by draw order
                for context in contexts_items do context.draw
        end
 
@@ -988,6 +1019,9 @@ private class SpriteContext
        # OpenGL ES usage of `buffer_array` and `buffer_element`
        var usage: GLBufferUsage
 
+       # Draw order shared by all `sprites`
+       var draw_order: Int
+
        # Sprites drawn by this context
        var sprites = new GroupedSprites
 
@@ -1177,8 +1211,12 @@ private class SpriteContext
                                data[o+36] = tc[v*2+1]
 
                                # a_tex_diff
-                               var dx = animation.frames[1].texture_coords[0] - animation.frames[0].texture_coords[0]
-                               var dy = animation.frames[1].texture_coords[1] - animation.frames[0].texture_coords[1]
+                               var dx = 0.0
+                               var dy = 0.0
+                               if animation.frames.length > 1 then
+                                       dx = animation.frames[1].texture_coords[0] - animation.frames[0].texture_coords[0]
+                                       dy = animation.frames[1].texture_coords[1] - animation.frames[0].texture_coords[1]
+                               end
                                data[o+37] = dx
                                data[o+38] = dy
 
@@ -1696,3 +1734,20 @@ redef class NativeGLfloatArray
                        self[i+dst_offset] = (GLfloat)matrix[i];
        `}
 end
+
+redef class Sys
+       private var sprite_draw_order_sorter = new DrawOrderComparator is lazy
+end
+
+# Sort `SpriteContext` by their `draw_order`
+private class DrawOrderComparator
+       super Comparator
+
+       # This class can't set COMPARED because
+       # `the public property cannot contain the private type...`
+       #redef type COMPARED: SpriteContext
+
+       # Require: `a isa SpriteContext and b isa SpriteContext`
+       redef fun compare(a, b)
+       do return a.as(SpriteContext).draw_order <=> b.as(SpriteContext).draw_order
+end