From: Alexis Laferrière Date: Sun, 2 Apr 2017 04:05:42 +0000 (-0400) Subject: gamnit: intro virtual gamepad X-Git-Url: http://nitlanguage.org gamnit: intro virtual gamepad Signed-off-by: Alexis Laferrière --- diff --git a/lib/gamnit/virtual_gamepad/Makefile b/lib/gamnit/virtual_gamepad/Makefile new file mode 100644 index 0000000..9eaa13e --- /dev/null +++ b/lib/gamnit/virtual_gamepad/Makefile @@ -0,0 +1,6 @@ +INKSCAPE_DIR ?= $(shell nitls -pP inkscape_tools) + +src/gamnit_touch_gamepad.nit: art/virtual_gamepad.svg + make -C ${INKSCAPE_DIR} + ${INKSCAPE_DIR}/bin/svg_to_png_and_nit -g --src virtual_gamepad_spritesheet.nit --scale 2.0 art/virtual_gamepad.svg + sed 's/Virtual_GamepadImages/VirtualGamepadSpritesheet/' -i virtual_gamepad_spritesheet.nit diff --git a/lib/gamnit/virtual_gamepad/art/virtual_gamepad.svg b/lib/gamnit/virtual_gamepad/art/virtual_gamepad.svg new file mode 100644 index 0000000..84de166 --- /dev/null +++ b/lib/gamnit/virtual_gamepad/art/virtual_gamepad.svg @@ -0,0 +1,2373 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/gamnit/virtual_gamepad/assets/images/gamnit_touch_gamepad.png b/lib/gamnit/virtual_gamepad/assets/images/gamnit_touch_gamepad.png new file mode 100644 index 0000000..69d885c Binary files /dev/null and b/lib/gamnit/virtual_gamepad/assets/images/gamnit_touch_gamepad.png differ diff --git a/lib/gamnit/virtual_gamepad/virtual_gamepad.nit b/lib/gamnit/virtual_gamepad/virtual_gamepad.nit new file mode 100644 index 0000000..2ffb84e --- /dev/null +++ b/lib/gamnit/virtual_gamepad/virtual_gamepad.nit @@ -0,0 +1,414 @@ +# 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. + +# Virtual gamepad mapped to keyboard keys for quick and dirty mobile support +# +# For Android, the texture is automatically added to the APK when this +# module is imported. However, due to a limitation of the _app.nit_ +# framework on desktop OS, the texture must be copied manually to the assets +# folder at `assets/images`, the texture is available at, from the repo root, +# `lib/gamnit/virtual_gamepad/assets/images`. +# +# The texture was created by kenney.nl and modified by Alexis Laferrière. +# It is published under CC0 and can be used and modified without attribution. +# +# ~~~ +# redef class App +# redef fun on_create +# do +# super +# +# # Create the virtual gamepad +# var gamepad = new VirtualGamepad +# +# # Configure it as needed +# gamepad.add_dpad(["w","a","s","d"]) +# gamepad.add_button("x", gamepad_spritesheet.x) +# gamepad.add_button("space", gamepad_spritesheet.star) +# +# # Assign it as the active gamepad +# self.gamepad = gamepad +# end +# +# fun setup_play_ui +# do +# # Show the virtual gamepad +# var gamepad = self.gamepad +# if gamepad != null then gamepad.visible = true +# end +# end +# ~~~ +module virtual_gamepad is app_files + +import flat +private import keys +import virtual_gamepad_spritesheet + +redef class App + + # Current touch gamepad, still may be invisible + var gamepad: nullable VirtualGamepad = null + + # Textures used for `DPad` and available to clients + var gamepad_spritesheet = new VirtualGamepadSpritesheet + + redef fun accept_event(event) + do + # Priority to the gamepad + var gamepad = gamepad + if gamepad != null and gamepad.accept_event(event) then return true + + return super + end +end + +# Gamepad on touch screen bound to keyboard keys +# +# Fires `VirtualGamepadEvent` which implement `KeyEvent` so it behaves like a keyboard. +class VirtualGamepad + + private var sprites = new Array[Sprite] + + # Controls composing this gamepad + # + # Controls can be added directly to this array or using `add_dpad` + # and `add_button`. + var controls = new Array[RoundControl] + + # Add a directional pad (`DPad`) to a default location + # + # The 4 buttons fire events with the corresponding name in `names`. + # Items in `names` should be in order of top, left, down and right. + # If `null`, defaults to WASD. + # + # If this method is called, it should be before `add_button` to + # avoid overlapping controls. + # + # A maximum of 2 `DPad` may be added using this method. + # The first `DPad` is placed on the left of the screen. + # The second `DPad` is on the right and replaces some buttons + # added by `add_button`. + # + # Require: `names == null or names.length == 4` + fun add_dpad(names: nullable Array[String]) + do + if names == null then names = ["w","a","s","d"] + assert names.length == 4 + + if n_dpads == 0 then + controls.add new DPad(app.ui_camera.bottom_left.offset(200.0, 100.0, 0.0), names) + else if n_dpads == 1 then + controls.add new DPad(app.ui_camera.bottom_right.offset(-200.0, 100.0, 0.0), names) + else + print_error "Too many DPad ({n_dpads}) in {self}" + end + end + + # Number of `DPad` in `controls` + private fun n_dpads: Int + do + var n_dpads = 0 + for c in controls do if c isa DPad then n_dpads += 1 + return n_dpads + end + + # Button positions for `add_button`, offsets from the bottom right + private var button_positions = new Array[Point[Float]].with_items( + new Point[Float](-150.0, 150.0), + new Point[Float](-150.0, 350.0), + new Point[Float](-150.0, 550.0), + new Point[Float](-350.0, 150.0), + new Point[Float](-350.0, 350.0), + new Point[Float](-350.0, 550.0)) + + # Add a round button to a default location + # + # Fired events use `name`, it should usually correspond to a + # keyboard key like "space" or "a". + # `texture` is displayed at the button position, it also sets the + # touchable surface of the button. + # + # If this method is called, it should be after `add_dpad` to + # avoid overlapping controls. + # + # A maximum of 6 buttons may be added using this method when + # there is less than 2 `DPad`. Otherwise, only 2 buttons can be added. + fun add_button(name: String, texture: Texture) + do + if n_dpads == 2 and button_positions.length == 6 then + # Drop the bottom 4 buttons + button_positions.remove_at 4 + button_positions.remove_at 3 + button_positions.remove_at 1 + button_positions.remove_at 0 + end + + assert button_positions.not_empty else print_error "Too many buttons in {self}" + var pos = button_positions.shift + controls.add new RoundButton( + app.ui_camera.bottom_right.offset(pos.x, pos.y, 0.0), name, texture) + end + + private fun prepare + do + var display = app.display + assert display != null + + for control in controls do + var sprites = control.sprites + app.ui_sprites.add_all sprites + end + end + + # Is this control visible? + var visible = false is private writable(visible_direct=) + + # Set this control to visible or not + fun visible=(value: Bool) + do + visible_direct = value + if value then show else hide + end + + private fun show + do + if sprites.is_empty then prepare + app.ui_sprites.add_all sprites + end + + private fun hide do for s in sprites do app.ui_sprites.remove_all s + + private var control_under_pointer = new Map[Int, RoundControl] + + private fun accept_event(event: InputEvent): Bool + do + if not visible then return false + + var display = app.display + if display == null then return false + + if event isa PointerEvent then + var ui_pos = app.ui_camera.camera_to_ui(event.x, event.y) + + for control in controls do + if control.accept_event(event, ui_pos) then + var prev_control = control_under_pointer.get_or_null(event.pointer_id) + if prev_control != null and prev_control != control then + prev_control.depressed_down + end + control_under_pointer[event.pointer_id] = control + return true + end + end + + var prev_control = control_under_pointer.get_or_null(event.pointer_id) + if prev_control != null then prev_control.depressed_down + control_under_pointer.keys.remove event.pointer_id + end + + return false + end +end + +# Event fired by a `VirtualGamepad` +class VirtualGamepadEvent + super KeyEvent + + redef var name + + redef var is_down is noautoinit +end + +# Control composing a `VirtualGamepad` +abstract class RoundControl + # Center position on the UI + var center: Point3d[Float] + + # Radius in UI units/pixels of the part of this control + fun radius: Float is abstract + + private fun sprites: Array[Sprite] do return new Array[Sprite] + + private fun accept_event(event: InputEvent, ui_pos: Point[Float]): Bool + do + if event isa PointerEvent and contains(ui_pos) then + return hit(event, ui_pos) + end + + return false + end + + # Does `self` contain a pointer at `ui_pos`? + private fun contains(ui_pos: Point[Float]): Bool + do + var dx = center.x - ui_pos.x + var dy = center.y - ui_pos.y + return dx*dx + dy*dy < radius*radius + end + + private fun hit(event: PointerEvent, ui_pos: Point[Float]): Bool + do return false + + # Keys currently down, to be depressed if the pointer moves away + private var down_names = new Set[String] + + # Depress/release keys kept down, listed by `down_names` + private fun depressed_down + do + for name in down_names do + var e = new VirtualGamepadEvent(name) + e.is_down = false + app.accept_event e + end + down_names.clear + end +end + +# Simple action button +class RoundButton + super RoundControl + + # Event name, should usually correspond to a keyboard key like "a" or "left" + var name: String + + # Texture drawn for this button, may be from `app.gamepad_spritesheet` + var texture: Texture + + redef fun radius do return 0.5*texture.height + + redef fun hit(event, ui_pos) + do + if not event.is_move then + var e = new VirtualGamepadEvent(name.to_s) + e.is_down = event.pressed + app.accept_event e + + if event.pressed then + down_names.add name + else down_names.clear + end + return true + end + + redef var sprites = [new Sprite(texture, center)] is lazy +end + +# Directional pad with up to 4 buttons +# +# Assumes that each pad is manipulated by at max a single pointer. +class DPad + super RoundControl + + # Event names for the keys, in order of top, left, down and right + var names: Array[String] + + # Show the up button + var show_up = true is writable + + # Show the down button + var show_down = true is writable + + # Show the left button + var show_left = true is writable + + # Show the right button + var show_right = true is writable + + redef fun radius do return 200.0 + + redef fun contains(ui_pos) + do + if show_down then return super(new Point[Float](ui_pos.x+0.0, ui_pos.y-100.0)) + return super + end + + redef fun hit(event, ui_pos) + do + var display = app.display + if display == null then return false + + var dx = ui_pos.x - center.x + var dy = ui_pos.y - center.y + if show_down then dy -= 100.0 + + # Angle (with 0.0 on the right) to index in WASD (0 -> W/up) + var indexes = new Set[Int] + var ao = atan2(dy, dx) + ao -= pi/4.0 + + # Look for 2 angles so 2 buttons can be pressed at the same time + for da in once [-pi/8.0, pi/8.0] do + var a = ao+da + while a < 0.0 do a += pi*2.0 + while a > 2.0*pi do a -= pi*2.0 + var index = (a * 2.0 / pi).floor.to_i + if index < 0 then index += 4 + indexes.add index + end + + var shows = [show_up, show_left, show_down, show_right] + var new_down_names = new Set[String] + for index in indexes do + # Don't trigger events for hidden buttons/directions + if not shows[index] then continue + + var name = names[index] + # Simulate event + var e = new VirtualGamepadEvent(name) + e.is_down = event.pressed + app.accept_event e + + if event.pressed then new_down_names.add name + end + + # Depress released directions + for name in down_names do + if not new_down_names.has(name) then + var e = new VirtualGamepadEvent(name) + e.is_down = false + app.accept_event e + end + end + + down_names = new_down_names + + return true + end + + redef fun sprites + do + var dy = 0.0 + if show_down then dy = 100.0 + + var sprites = new Array[Sprite] + + # Interactive buttons + if show_up then sprites.add new Sprite(app.gamepad_spritesheet.dpad_up, + center.offset( 0.0, 100.0+dy, 0.0)) + if show_left then sprites.add new Sprite(app.gamepad_spritesheet.dpad_left, + center.offset(-100.0, 0.0+dy, 0.0)) + if show_right then sprites.add new Sprite(app.gamepad_spritesheet.dpad_right, + center.offset( 100.0, 0.0+dy, 0.0)) + if show_down then sprites.add new Sprite(app.gamepad_spritesheet.dpad_down, + center.offset( 0.0,-100.0+dy, 0.0)) + + # Non-interactive joystick background + sprites.add new Sprite(app.gamepad_spritesheet.joystick_back, + center.offset(0.0, 0.0+dy, -1.0)) # In the back + if not show_down then sprites.add new Sprite(app.gamepad_spritesheet.joystick_down, + center.offset(0.0, -100.0+dy, 0.0)) + + return sprites + end +end diff --git a/lib/gamnit/virtual_gamepad/virtual_gamepad_spritesheet.nit b/lib/gamnit/virtual_gamepad/virtual_gamepad_spritesheet.nit new file mode 100644 index 0000000..64c0048 --- /dev/null +++ b/lib/gamnit/virtual_gamepad/virtual_gamepad_spritesheet.nit @@ -0,0 +1,33 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# File generated by svg_to_png_and_nit, do not modify, redef instead + +import gamnit::textures + +class VirtualGamepadSpritesheet + + private var root_texture = new Texture("images/virtual_gamepad.png") + var a: Texture = root_texture.subtexture(856, 816, 162, 162) + var b: Texture = root_texture.subtexture(1036, 816, 162, 162) + var cancel: Texture = root_texture.subtexture(1036, 996, 162, 162) + var down: Texture = root_texture.subtexture(1216, 636, 162, 162) + var dpad_down: Texture = root_texture.subtexture(1136, 446, 124, 152) + var dpad_left: Texture = root_texture.subtexture(1036, 296, 154, 124) + var dpad_right: Texture = root_texture.subtexture(1206, 296, 152, 124) + var dpad_up: Texture = root_texture.subtexture(1136, 116, 124, 154) + var fire: Texture = root_texture.subtexture(1396, 996, 162, 162) + var fist: Texture = root_texture.subtexture(1216, 996, 162, 162) + var flag: Texture = root_texture.subtexture(1576, 816, 162, 162) + var joystick_back: Texture = root_texture.subtexture(0, 0, 194, 194) + var joystick_down: Texture = root_texture.subtexture(784, 30, 124, 152) + var key: Texture = root_texture.subtexture(1576, 636, 162, 162) + var left: Texture = root_texture.subtexture(676, 636, 162, 162) + var ok: Texture = root_texture.subtexture(856, 996, 162, 162) + var pedal: Texture = root_texture.subtexture(1576, 996, 162, 162) + var right: Texture = root_texture.subtexture(856, 636, 162, 162) + var star: Texture = root_texture.subtexture(1396, 636, 162, 162) + var turn_left: Texture = root_texture.subtexture(676, 816, 162, 162) + var turn_right: Texture = root_texture.subtexture(676, 996, 162, 162) + var up: Texture = root_texture.subtexture(1036, 636, 162, 162) + var x: Texture = root_texture.subtexture(1216, 816, 162, 162) + var y: Texture = root_texture.subtexture(1396, 816, 162, 162) +end