X-Git-Url: http://nitlanguage.org diff --git a/lib/gamnit/flat.nit b/lib/gamnit/flat.nit index c9935d8..335bbd1 100644 --- a/lib/gamnit/flat.nit +++ b/lib/gamnit/flat.nit @@ -30,25 +30,70 @@ # devices. It is used to position the sprites in `App::ui_sprites`. # # See the sample game at `contrib/asteronits/` and the basic project template -# at `lib/gamnit/examples/template_flat/`. +# at `lib/gamnit/examples/template/`. module flat import glesv2 intrude import geometry::points_and_lines # For _x, _y and _z +intrude import matrix import matrix::projection import more_collections import performance_analysis import gamnit -import gamnit::cameras +intrude import gamnit::cameras +intrude import gamnit::cameras_cache +import gamnit::dynamic_resolution import gamnit::limit_fps import gamnit::camera_control -# Draw a `texture` at `center` +# Visible 2D entity in the game world or UI # -# An instance of `Sprite` can only belong to a single `SpriteSet` at -# a time. The on screen position depends on the camera associated +# Similar to `gamnit::Actor` which is in 3D. +# +# Each sprite associates a `texture` to the position `center`. +# The appearance is modified by `rotation`, `invert_x`, +# `scale`, `red`, `green`, `blue` and `alpha`. +# These values can be changed at any time and will trigger an update +# of the data on the GPU side, having a small performance cost. +# +# For a sprite to be visible, it must be added to either the world `sprites` +# or the `ui_sprites`. +# However, an instance of `Sprite` can only belong to a single `SpriteSet` +# at a time. The final on-screen position depends on the camera associated # to the `SpriteSet`. +# +# ~~~ +# # Load texture and create sprite +# var texture = new Texture("path/in/assets.png") +# var sprite = new Sprite(texture, new Point3d[Float](0.0, 0.0, 0.0)) +# +# # Add sprite to the visible game world +# app.sprites.add sprite +# +# # Extra configuration of the sprite +# sprite.rotation = pi/2.0 +# sprite.scale = 2.0 +# +# # Show only the blue colors +# sprite.red = 0.0 +# sprite.green = 0.0 +# ~~~ +# +# To add a sprite to the UI it can be anchored to screen borders +# with `ui_camera.top_left` and the likes. +# +# ~~~nitish +# # Place it a bit off the top left of the screen +# var pos = app.ui_camera.top_left.offset(128.0, -128.0, 0) +# +# # Load texture and create sprite +# var texture = new Texture("path/in/assets.png") +# var sprite = new Sprite(texture, pos) +# +# # Add it to the UI (above world sprites) +# app.ui_sprites.add sprite +# ~~~ class Sprite # Texture drawn to screen @@ -78,10 +123,10 @@ class Sprite center_direct = value end - # Rotation on the Z axis, positive values go counterclockwise + # Rotation on the Z axis, positive values turn counterclockwise var rotation = 0.0 is writable(rotation_direct=) - # Rotation on the Z axis, positive values go counterclockwise + # Rotation on the Z axis, positive values turn counterclockwise fun rotation=(value: Float) do if isset _rotation and value != rotation then needs_update @@ -100,10 +145,12 @@ class Sprite # Scale applied to this sprite # - # The default size of `self` depends on the size in pixels of `texture`. + # The basic size of `self` depends on the size in pixels of `texture`. var scale = 1.0 is writable(scale_direct=) - # Scale applied to this sprite, see `scale` + # Scale applied to this sprite + # + # The basic size of `self` depends on the size in pixels of `texture`. fun scale=(value: Float) do if isset _scale and value != scale then needs_update @@ -188,7 +235,10 @@ class Sprite fun needs_update do var c = context - if c != null then c.sprites_to_update.add self + if c == null then return + if c.last_sprite_to_update == self then return + c.sprites_to_update.add self + c.last_sprite_to_update = self end # Request a resorting of this sprite in its sprite list @@ -206,6 +256,9 @@ class Sprite # Current context to which `self` was sorted private var context: nullable SpriteContext = null + # Index in `context` + private var context_index: Int = -1 + # Current context to which `self` belongs private var sprite_set: nullable SpriteSet = null end @@ -216,25 +269,25 @@ redef class App # Camera for world `sprites` and `depth::actors` with perspective # - # By default, the camera is configured to respect the resolution - # of the screen in world coordinates at `z == 0.0`. + # By default, the camera is configured to a height of 1080 units + # of world coordinates at `z == 0.0`. var world_camera: EulerCamera is lazy do var camera = new EulerCamera(app.display.as(not null)) - # Aim for pixel resolution at level 0 - camera.reset_height - camera.near = 100.0 + # Aim for full HD pixel resolution at level 0 + camera.reset_height 1080.0 + camera.near = 10.0 return camera end # Camera for `ui_sprites` using an orthogonal view - var ui_camera: UICamera = new UICamera(app.display.as(not null)) is lazy + var ui_camera = new UICamera(app.display.as(not null)) is lazy - # World sprites to draw as seen by `world_camera` + # World sprites drawn as seen by `world_camera` var sprites: Set[Sprite] = new SpriteSet - # UI sprites to draw as seen by `ui_camera`, drawn over world `sprites` + # UI sprites drawn as seen by `ui_camera`, over world `sprites` var ui_sprites: Set[Sprite] = new SpriteSet # Main method to refine in clients to update game logic and `sprites` @@ -247,9 +300,7 @@ redef class App do texture.load - ui_camera.reset_height 1080.0 - - var splash = new Sprite(texture, ui_camera.center) + var splash = new Sprite(texture, ui_camera.center.offset(0.0, 0.0, 0.0)) ui_sprites.add splash var display = display @@ -327,11 +378,19 @@ redef class App if display != null then display.close end - redef fun frame_core(display) + redef fun on_resize(display) do - # Prepare to draw, clear buffers - glClear(gl_COLOR_BUFFER_BIT | gl_DEPTH_BUFFER_BIT) + super + world_camera.mvp_matrix_cache = null + ui_camera.mvp_matrix_cache = null + + # Update all sprites in the UI + for sprite in ui_sprites do sprite.needs_update + end + + redef fun frame_core(display) + do # Check errors var gl_error = glGetError assert gl_error == gl_NO_ERROR else print_error gl_error @@ -354,13 +413,16 @@ redef class App # Draw the whole screen, all `glDraw...` calls should be executed here protected fun frame_core_draw(display: GamnitDisplay) do - perf_clock_main.lapse + frame_core_dynamic_resolution_before display + perf_clock_main.lapse frame_core_world_sprites display perfs["gamnit flat world_sprites"].add perf_clock_main.lapse frame_core_ui_sprites display perfs["gamnit flat ui_sprites"].add perf_clock_main.lapse + + frame_core_dynamic_resolution_after display end private fun frame_core_sprites(display: GamnitDisplay, sprite_set: SpriteSet, camera: Camera) @@ -491,7 +553,7 @@ private class Simple2dProgram { if(use_texture) { gl_FragColor = v_color * texture2D(texture0, v_coord); - if (gl_FragColor.a <= 0.1) discard; + if (gl_FragColor.a <= 0.01) discard; } else { gl_FragColor = v_color; } @@ -536,12 +598,6 @@ private class Simple2dProgram end redef class Point3d[N] - # Get a new `Point3d[Float]` with an offset of each axis of `x, y, z` - fun offset(x, y, z: Numeric): Point3d[Float] - do - return new Point3d[Float](self.x.to_f+x.to_f, self.y.to_f+y.to_f, self.z.to_f+z.to_f) - end - # --- # Associate each point to its sprites @@ -592,12 +648,32 @@ redef class Point3d[N] end end +redef class OffsetPoint3d + redef fun x=(v) + do + if isset _x and v != x then needs_update + super + end + + redef fun y=(v) + do + if isset _y and v != y then needs_update + super + end + + redef fun z=(v) + do + if isset _z and v != z then needs_update + super + end +end + # Set of sprites sorting them into different `SpriteContext` private class SpriteSet super HashSet[Sprite] # Map texture then static vs dynamic to a `SpriteContext` - var contexts_map = new HashMap2[GamnitRootTexture, Bool, SpriteContext] + var contexts_map = new HashMap2[RootTexture, Bool, SpriteContext] # Contexts in `contexts_map` var contexts_items = new Array[SpriteContext] @@ -623,6 +699,7 @@ private class SpriteSet context.sprites.add sprite context.sprites_to_update.add sprite + context.last_sprite_to_update = sprite sprite.context = context sprite.sprite_set = self @@ -672,6 +749,10 @@ private class SpriteSet redef fun clear do + for sprite in self do + sprite.context = null + sprite.sprite_set = null + end super for c in contexts_items do c.destroy contexts_map.clear @@ -688,17 +769,20 @@ private class SpriteContext # Context config and state # Only root texture drawn by this context - var texture: nullable GamnitRootTexture + var texture: nullable RootTexture # OpenGL ES usage of `buffer_array` and `buffer_element` var usage: GLBufferUsage # Sprites drawn by this context - var sprites = new GroupedArray[Sprite] + var sprites = new GroupedSprites # Sprites to update since last `draw` var sprites_to_update = new Set[Sprite] + # Cache of the last `Sprite` added to `sprites_to_update` since the last call to `draw` + var last_sprite_to_update: nullable Sprite = null + # Sprites that have been update and for which `needs_update` can be set to false var updated_sprites = new Array[Sprite] @@ -804,8 +888,11 @@ private class SpriteContext # Update GPU data of `sprite` fun update_sprite(sprite: Sprite) do - var sprite_index = sprites.index_of(sprite) - if sprite_index == -1 then return + var context = sprite.context + if context != self then return + + var sprite_index = sprite.context_index + assert sprite_index != -1 # Vertices data @@ -851,7 +938,7 @@ private class SpriteContext else rot = new Matrix.rotation(sprite.rotation, 0.0, 0.0, 1.0) end - data.fill_from(rot.items, o+15) + data.fill_from_matrix(rot, o+15) o += float_per_vertex end @@ -903,6 +990,7 @@ private class SpriteContext glBindBuffer(gl_ELEMENT_ARRAY_BUFFER, buffer_element) # Resize GPU buffers? + var update_everything = false if sprites.capacity > buffer_capacity then # Try to defragment first var moved = sprites.defragment @@ -912,6 +1000,7 @@ private class SpriteContext resize # We must update everything + update_everything = true for s in sprites.items do if s != null then sprites_to_update.add s else # Just update the moved sprites @@ -925,11 +1014,19 @@ private class SpriteContext end # Update GPU sprites data - if sprites_to_update.not_empty then + if sprites_to_update.not_empty or update_everything then app.perf_clock_sprites.lapse - for sprite in sprites_to_update do update_sprite(sprite) + if update_everything then + for sprite in sprites.items do if sprite != null then + update_sprite(sprite) + end + else + for sprite in sprites_to_update do update_sprite(sprite) + end + sprites_to_update.clear + last_sprite_to_update = null sys.perfs["gamnit flat gpu update"].add app.perf_clock_sprites.lapse end @@ -1031,6 +1128,8 @@ end # The data can be compressed by a call to `defragment`. # # ~~~ +# intrude import gamnit::flat +# # var array = new GroupedArray[String] # assert array.to_s == "" # @@ -1069,7 +1168,7 @@ end # # array.add "E" # assert array.to_s == "[A,B,c,D,E]" -# assert array.capacity == 5 +# assert array.capacity == 6 # assert array.length == 5 # # array.remove "A" @@ -1093,9 +1192,6 @@ private class GroupedArray[E] # Number of item slots in the array fun capacity: Int do return items.length - # Index of `item` - fun index_of(item: E): Int do return items.index_of(item) - # List of available slots var available = new MinHeap[Int].default @@ -1105,8 +1201,8 @@ private class GroupedArray[E] # Index of the spots after filled chunks var ends = new List[Int] - # Add `item` to the first available slot - fun add(item: E) + # Add `item` to the first available slot and return its index + fun add(item: E): Int do length += 1 @@ -1131,7 +1227,7 @@ private class GroupedArray[E] # at end of first chunk ends.first += 1 end - return + return i end items.add item @@ -1139,13 +1235,20 @@ private class GroupedArray[E] starts.add 0 ends.add 1 else ends.last += 1 + return ends.last - 1 end # Remove the first instance of `item` fun remove(item: E) do - var i = items.index_of(item) - assert i != -1 + var index = items.index_of(item) + remove_at(item, index) + end + + # Remove `item` at `index` + fun remove_at(item: E, index: Int) + do + var i = index length -= 1 items[i] = null @@ -1190,6 +1293,8 @@ private class GroupedArray[E] # Returns the elements that moved as a list. # # ~~~ + # intrude import gamnit::flat + # # var array = new GroupedArray[String] # array.add "a" # array.add "b" @@ -1218,6 +1323,7 @@ private class GroupedArray[E] while max > 0 and (starts.length > 1 or starts.first != 0) do var i = ends.last - 1 var e = items[i] + assert e != null remove e add e moved.add e @@ -1249,3 +1355,35 @@ private class GroupedArray[E] return ss.join end end + +# Optimized `GroupedArray` to use `Sprite::context_index` and avoid using `index_of` +private class GroupedSprites + super GroupedArray[Sprite] + + redef fun add(item) + do + var index = super + item.context_index = index + return index + end + + redef fun remove(item) do remove_at(item, item.context_index) +end + +redef class GLfloatArray + private fun fill_from_matrix(matrix: Matrix, dst_offset: nullable Int) + do + dst_offset = dst_offset or else 0 + var mat_len = matrix.width*matrix.height + assert length >= mat_len + dst_offset + native_array.fill_from_matrix_native(matrix.items, dst_offset, mat_len) + end +end + +redef class NativeGLfloatArray + private fun fill_from_matrix_native(matrix: matrix::NativeDoubleArray, dst_offset, len: Int) `{ + int i; + for (i = 0; i < len; i ++) + self[i+dst_offset] = (GLfloat)matrix[i]; + `} +end