From: Alexis Laferrière Date: Sat, 26 Sep 2015 18:40:14 +0000 (-0400) Subject: lib/gamnit: intro `simple_2d` and API for easy 2D games programming X-Git-Tag: v0.8~36^2~10 X-Git-Url: http://nitlanguage.org lib/gamnit: intro `simple_2d` and API for easy 2D games programming Signed-off-by: Alexis Laferrière --- diff --git a/lib/gamnit/simple_2d.nit b/lib/gamnit/simple_2d.nit new file mode 100644 index 0000000..75aac5e --- /dev/null +++ b/lib/gamnit/simple_2d.nit @@ -0,0 +1,341 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Simple API for 2D games, based around `Sprite` and `App::update` +# +# Client programs should implement `App::update` to execute game logic and +# add instances of `Sprite` to `App::sprites` and `App::ui_sprites`. +# At each frame, all sprites are drawn to the screen. +# +# This system relies on two cameras `App::world_camera` and `App::ui_camera`. +# +# * `App::world_camera` applies a perspective effect to draw the game world. +# This camera is designed to be moved around to see the world as well as to zoom in and out. +# It is used to position the sprites in `App::sprites`. +# +# * `App::ui_camera` is a simple orthogonal camera to display UI objects. +# This camera should mostly be still, it can still move for chock effects and the like. +# It can be used to standardize the size of the UI across devices. +# It is used to position the sprites in `App::ui_sprites`. +# +# See the sample game at `examples/asteronits/`. +module simple_2d + +import glesv2 + +import geometry::points_and_lines +import matrix::projection +import more_collections +import realtime + +import gamnit +import gamnit::cameras +import gamnit::limit_fps + +# Image to draw on screen +class Sprite + + # Texture drawn to screen + var texture: Texture is writable + + # Position of this sprite in world coordinates + var center: Point3d[Float] is writable + + # Rotation on the Z axis + var rotation = 0.0 is writable + + # Scale applied to this sprite + var scale = 1.0 is writable + + # Transparency applied to the texture on draw + var alpha = 1.0 is writable + + private fun draw + do + var simple_2d_program = app.simple_2d_program + + glActiveTexture gl_TEXTURE0 + glBindTexture(gl_TEXTURE_2D, texture.root.gl_texture) + + simple_2d_program.translation.uniform(center.x, -center.y, center.z, 0.0) + simple_2d_program.color.uniform(1.0, 1.0, 1.0, alpha) + simple_2d_program.scale.uniform scale + + simple_2d_program.use_texture.uniform true + simple_2d_program.texture.uniform 0 + simple_2d_program.tex_coord.array(texture.texture_coords, 2) + simple_2d_program.coord.array(texture.vertices, 3) + + simple_2d_program.rotation.uniform new Matrix.rotation(rotation, 0.0, 0.0, 1.0) + + glDrawArrays(gl_TRIANGLE_STRIP, 0, 4) + end +end + +redef class App + # Default graphic program to draw `sprites` + var simple_2d_program = new Simple2dProgram is lazy # TODO private + + # Camera for world objects with perspective + # + # By default, the camera is configured to respect the resolution + # of the screen in 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 + + return camera + end + + # Camera for UI elements using an orthogonal view + var ui_camera: UICamera = new UICamera(app.display.as(not null)) is lazy + + # Live sprites to draw in reference to `world_camera` + var sprites: Sequence[Sprite] = new List[Sprite] + + # UI sprites to draw in reference to `ui_camera`, over world `sprites` + var ui_sprites: Sequence[Sprite] = new List[Sprite] + + private var clock = new Clock + + redef fun on_create + do + super + + var display = display + assert display != null + + var gl_error = glGetError + assert gl_error == gl_NO_ERROR else print gl_error + + # Prepare program + var program = simple_2d_program + program.compile_and_link + + var gamnit_error = program.error + assert gamnit_error == null else print_error gamnit_error + + # Enable blending + gl.capabilities.blend.enable + glBlendFunc(gl_SRC_ALPHA, gl_ONE_MINUS_SRC_ALPHA) + + # Enable depth test + gl.capabilities.depth_test.enable + glDepthFunc gl_LEQUAL + glDepthMask true + + # Prepare viewport and background color + glViewport(0, 0, display.width, display.height) + glClearColor(0.0, 0.0, 0.0, 1.0) + + gl_error = glGetError + assert gl_error == gl_NO_ERROR else print gl_error + + # Prepare to draw + for tex in all_root_textures do + tex.load + + glTexParameteri(gl_TEXTURE_2D, gl_TEXTURE_MIN_FILTER, gl_LINEAR) + glTexParameteri(gl_TEXTURE_2D, gl_TEXTURE_MAG_FILTER, gl_LINEAR) + + gamnit_error = tex.error + assert gamnit_error == null else print_error gamnit_error + end + + # Constant program values + program.use + program.coord.array_enabled = true + program.tex_coord.array_enabled = true + + gl_error = glGetError + assert gl_error == gl_NO_ERROR else print gl_error + end + + redef fun frame_core(display) + do + # Prepare to draw, clear buffers + glClear(gl_COLOR_BUFFER_BIT | gl_DEPTH_BUFFER_BIT) + + # Check errors + var gl_error = glGetError + assert gl_error == gl_NO_ERROR else print gl_error + + # Update game logic and set sprites + var dt = clock.lapse.to_f + update dt + + # Draw and flip screen + # TODO optimize this draw to store constant values on the GPU + + ## World sprites + simple_2d_program.use + simple_2d_program.mvp.uniform world_camera.mvp_matrix + for sprite in sprites do sprite.draw + + ## Reset only the depth buffer + glClear gl_DEPTH_BUFFER_BIT + + ## UI sprites + simple_2d_program.mvp.uniform ui_camera.mvp_matrix + for sprite in ui_sprites do sprite.draw + display.flip + + # Check errors + gl_error = glGetError + assert gl_error == gl_NO_ERROR else print gl_error + end + + # Main method to refine in clients to update game logic and `sprites` + fun update(dt: Float) do end + + redef fun on_stop + do + # Clean up + simple_2d_program.delete + + # Close gamnit + var display = display + if display != null then display.close + end +end + +redef class Texture + + # Vertices coordinates of the base geometry + private var vertices: Array[Float] is lazy do + var mod = 1.0 + var w = width * mod + var h = height * mod + var a = [-0.5*w, -0.5*h, 0.0] + var b = [ 0.5*w, -0.5*h, 0.0] + var c = [-0.5*w, 0.5*h, 0.0] + var d = [ 0.5*w, 0.5*h, 0.0] + + var vertices = new Array[Float] + for v in [c, d, a, b] do vertices.add_all v + return vertices + end + + # Coordinates of this texture on the `root` texture, with `[0..1.0]` + private var texture_coords: Array[Float] is lazy do + var a = [offset_left, offset_bottom] + var b = [offset_right, offset_bottom] + var c = [offset_left, offset_top] + var d = [offset_right, offset_top] + + var texture_coords = new Array[Float] + for v in [c, d, a, b] do texture_coords.add_all v + return texture_coords + end +end + +# Graphic program to display simple models with a texture, translation, rotation and scale +class Simple2dProgram + super GamnitProgramFromSource + + redef var vertex_shader_source = """ + // Vertex coordinates + attribute vec4 coord; + + // Vertex color tint + attribute vec4 color; + + // Vertex translation + attribute vec4 translation; + + // Vertex scaling + attribute float scale; + + // Vertex coordinates on textures + attribute vec2 tex_coord; + + // Model view projection matrix + uniform mat4 mvp; + + // Rotation matrix + uniform mat4 rotation; + + // Output for the fragment shader + varying vec4 v_color; + varying vec2 v_coord; + + void main() + { + v_color = color; + gl_Position = (vec4(coord.xyz * scale, 1.0) * rotation + translation) * mvp; + v_coord = tex_coord; + } + """ @ glsl_vertex_shader + + redef var fragment_shader_source = """ + precision mediump float; + + // Does this object use a texture? + uniform bool use_texture; + + // Texture to apply on this object + uniform sampler2D texture; + + // Input from the vertex shader + varying vec4 v_color; + varying vec2 v_coord; + + void main() + { + if(use_texture) { + gl_FragColor = v_color * texture2D(texture, v_coord); + if (gl_FragColor.a == 0.0) discard; + } else { + gl_FragColor = v_color; + } + } + """ @ glsl_fragment_shader + + # Vertices coordinates + var coord = attributes["coord"].as(AttributeVec4) is lazy + + # Should this program use the texture `texture`? + var use_texture = uniforms["use_texture"].as(UniformBool) is lazy + + # Visible texture unit + var texture = uniforms["texture"].as(UniformSampler2D) is lazy + + # Coordinates on the textures, per vertex + var tex_coord = attributes["tex_coord"].as(AttributeVec2) is lazy + + # Color tint per vertex + var color = attributes["color"].as(AttributeVec4) is lazy + + # Translation applied to each vertex + var translation = attributes["translation"].as(AttributeVec4) is lazy + + # Rotation matrix + var rotation = uniforms["rotation"].as(UniformMat4) is lazy + + # Scaling per vertex + var scale = attributes["scale"].as(AttributeFloat) is lazy + + # Model view projection matrix + var mvp = uniforms["mvp"].as(UniformMat4) is lazy +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 +end