From: Jean Privat Date: Tue, 11 Apr 2017 12:30:27 +0000 (-0400) Subject: Merge: gamnit: virtual gamepad X-Git-Url: http://nitlanguage.org?hp=c915cfadfb71bdb6cdd15090b9ede4e1179e1690 Merge: gamnit: virtual gamepad Intro an easy to use virtual gamepad for quick ports of desktop games to mobiles devices with a touchscreen only. The gamepad is composed of up to 2 d-pads and up to 6 action buttons. Each button and direction on the pad is mapped to customizable keyboard keys. As such, it is ideal to simulate keyboard inputs on mobile devices. The `virtual_gamepad` module uses the `app_files` annotation which automatically includes the required PNG files in Android's APK files when the module is imported. The virtual gamepad is derived from the one in Asteronits, which it replaced. It is now used in Asteronits and Action Nitro, and it will be added to Devil's Avocados as soon as this is merged. The gamepad comes with a selection of button allowing for some customization, however custom buttons are also supported. Usage example from Devil's Avocados: ~~~ var gamepad = new VirtualGamepad gamepad.add_dpad gamepad.add_button("q", gamepad_spritesheet.turn_left) gamepad.add_button("e", gamepad_spritesheet.turn_right) gamepad.add_button("space", gamepad_spritesheet.star) gamepad.visible = true app.gamepad = gamepad ~~~ And the result, on Android: ![screenshot_20170404-125407](https://cloud.githubusercontent.com/assets/208057/24668912/641d091a-1936-11e7-81ff-eaf026540db6.png) This version should manage quite well the edge cases of touchscreen controls, which explains the complexity of `virtual_gamepad.nit`. However it is still limited, future work could add: - Virtual analog joystick, it would be easier to code than the d-pad. - Abstraction for gamepads (and analog joysticks). This would require unification with the current Android physical gamepad support and implementing desktop physical gamepad support with SDL2. Pull-Request: #2405 Reviewed-by: Romain Chanoir Reviewed-by: Jean Privat --- diff --git a/contrib/action_nitro/Makefile b/contrib/action_nitro/Makefile index 59b2d87..b3fcebc 100644 --- a/contrib/action_nitro/Makefile +++ b/contrib/action_nitro/Makefile @@ -3,9 +3,13 @@ NITLS=nitls all: bin/action_nitro -bin/action_nitro: $(shell ${NITLS} -M src/action_nitro.nit linux) pre-build +bin/action_nitro: $(shell ${NITLS} -M src/action_nitro.nit -m linux) pre-build ${NITC} src/action_nitro.nit -m linux -o $@ +android: +bin/action_nitro.apk: $(shell ${NITLS} -M src/action_nitro.nit -m android -m src/touch_ui.nit) pre-build android/res/ + ${NITC} src/action_nitro.nit -m android -m src/touch_ui.nit -o $@ + src/gen/texts.nit: art/texts.svg make -C ../inkscape_tools/ ../inkscape_tools/bin/svg_to_png_and_nit art/texts.svg -a assets/ -s src/gen/ -x 2.0 -g @@ -14,6 +18,10 @@ src/gen/planes.nit: art/planes.svg make -C ../inkscape_tools/ ../inkscape_tools/bin/svg_to_png_and_nit art/planes.svg -a assets/ -s src/gen/ -x 2.0 -g +android/res/: art/icon.svg + make -C ../inkscape_tools/ + ../inkscape_tools/bin/svg_to_icons --out android/res --android art/icon.svg + pre-build: src/gen/texts.nit src/gen/planes.nit check: bin/action_nitro diff --git a/contrib/action_nitro/art/icon.svg b/contrib/action_nitro/art/icon.svg new file mode 100644 index 0000000..7383208 --- /dev/null +++ b/contrib/action_nitro/art/icon.svg @@ -0,0 +1,508 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/contrib/action_nitro/art/planes.svg b/contrib/action_nitro/art/planes.svg index 6b6fef3..5835292 100644 --- a/contrib/action_nitro/art/planes.svg +++ b/contrib/action_nitro/art/planes.svg @@ -7,6 +7,7 @@ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" id="svg2998" @@ -200,7 +201,59 @@ is_visible="true" /> \ No newline at end of file + inkscape:connector-curvature="0" /> \ No newline at end of file diff --git a/contrib/action_nitro/net.xymus.action_nitro.txt b/contrib/action_nitro/net.xymus.action_nitro.txt new file mode 100644 index 0000000..2acd516 --- /dev/null +++ b/contrib/action_nitro/net.xymus.action_nitro.txt @@ -0,0 +1,11 @@ +Categories:Nit,Games +License:Apache2 +Web Site:http://nitlanguage.org +Source Code:http://nitlanguage.org/nit.git/tree/HEAD:/contrib/action_nitro +Issue Tracker:https://github.com/nitlang/nit/issues + +Summary:Jump from plate to plane to reach the ISS and defeat the bad guys +Description: +Simple action platformer using many features of the gamnit game framework. +This was created by the Nit team in a weekend for the clibre gamejam 2016. +. diff --git a/contrib/action_nitro/src/action_nitro.nit b/contrib/action_nitro/src/action_nitro.nit index b780a5d..25ed15d 100644 --- a/contrib/action_nitro/src/action_nitro.nit +++ b/contrib/action_nitro/src/action_nitro.nit @@ -12,7 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -module action_nitro +module action_nitro is + app_name "Action Nitro" + app_namespace "net.xymus.action_nitro" + app_version(1, 0, git_revision) + + android_manifest_activity """android:screenOrientation="sensorLandscape"""" + android_api_target 15 +end import gamnit::depth import gamnit::keys @@ -32,7 +39,7 @@ redef class App # Game world assets # Textures of the biplane, jet, helicopter, parachute and powerups - private var planes_sheet = new PlanesImages + var planes_sheet = new PlanesImages # Animation for the player movement private var player_textures: Array[Texture] = @@ -122,6 +129,9 @@ redef class App private var altitude_counter = new CounterSprites(texts_sheet.n, new Point3d[Float](1400.0, -64.0, 0.0)) + # Did the player asked to skip the intro animation? + private var skip_intro = false + redef fun on_create do super @@ -240,7 +250,7 @@ redef class App # Cinematic? var t = world.t var intro_duration = 8.0 - if t < intro_duration then + if t < intro_duration and not skip_intro then var pitch = t/intro_duration pitch = (pitch*pi).sin world_camera.pitch = pitch @@ -248,16 +258,10 @@ redef class App end if world.player == null then - # Game is starting! - world.spawn_player - world.planes.add new Airplane(new Point3d[Float](0.0, world.player.center.y - 10.0, 0.0), 16.0, 4.0) - - # Setup tutorial - ui_sprites.clear - ui_sprites.add_all([tutorial_wasd, tutorial_arrows, tutorial_chute]) - world_camera.pitch = 0.0 world_camera.far = 700.0 + + begin_play true end # Update counters @@ -336,6 +340,20 @@ redef class App end end + # Begin playing, after intro if `initial`, otherwise after death + fun begin_play(initial: Bool) + do + ui_sprites.clear + + world.spawn_player + world.planes.add new Airplane(new Point3d[Float](0.0, world.player.center.y - 10.0, 0.0), 16.0, 4.0) + + if initial then + # Setup tutorial + ui_sprites.add_all([tutorial_wasd, tutorial_arrows, tutorial_chute]) + end + end + # Seconds at which the game was won, using `world.t` as reference private var won_at: nullable Float = null @@ -350,7 +368,7 @@ redef class App redef fun accept_event(event) do - var s = super + if super then return true if event isa QuitEvent then print perfs @@ -397,17 +415,20 @@ redef class App else player.sprite.as(PlayerSprite).start_running end end + end - # When player is dead, respawn on spacebar - if player != null and not player.is_alive then - if event.name == "space" then - ui_sprites.clear - world.spawn_player - end + # When player is dead, respawn on spacebar or pointer depressed + if (event isa KeyEvent and event.name == "space") or + (event isa PointerEvent and not event.is_move and event.depressed) then + var player = world.player + if player == null then + skip_intro = true + else if not player.is_alive then + begin_play false end end - return s + return false end end diff --git a/contrib/action_nitro/src/game/core.nit b/contrib/action_nitro/src/game/core.nit index 9e437a1..25cf457 100644 --- a/contrib/action_nitro/src/game/core.nit +++ b/contrib/action_nitro/src/game/core.nit @@ -238,7 +238,7 @@ abstract class Body end # Destroy this objects and most references to it - protected fun destroy(world: World) do end + fun destroy(world: World) do end # --- # Box services diff --git a/contrib/action_nitro/src/game/planegen.nit b/contrib/action_nitro/src/game/planegen.nit index 94cb237..667783c 100644 --- a/contrib/action_nitro/src/game/planegen.nit +++ b/contrib/action_nitro/src/game/planegen.nit @@ -40,7 +40,7 @@ redef class World for i in planes.reverse_iterator do if i.out_of_screen(p, self) then #print "Despawning plane" - i.die(self) + i.destroy self end end @@ -118,14 +118,14 @@ redef class World if p == null then return if p.altitude >= boss_altitude then for e in enemies.reverse_iterator do if e isa JetpackEnemy then - e.die(self) + e.destroy self end return end for i in enemies.reverse_iterator do if i.out_of_screen(p, self) then #print "Despawning enemy" - i.die(self) + i.destroy self end end diff --git a/contrib/action_nitro/src/touch_ui.nit b/contrib/action_nitro/src/touch_ui.nit new file mode 100644 index 0000000..b97be7e --- /dev/null +++ b/contrib/action_nitro/src/touch_ui.nit @@ -0,0 +1,38 @@ +# 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. + +import gamnit::virtual_gamepad + +import action_nitro + +redef class App + redef fun on_create + do + super + + var gamepad = new VirtualGamepad + gamepad.add_dpad(["up","left","down","right"]) + gamepad.add_dpad(["w","a","s","d"]) + gamepad.add_button("space", planes_sheet.open_chute) + self.gamepad = gamepad + end + + redef fun begin_play(initial) + do + super + + var gamepad = self.gamepad + if gamepad != null then gamepad.visible = true + end +end diff --git a/contrib/asteronits/Makefile b/contrib/asteronits/Makefile index fef1d76..aee8027 100644 --- a/contrib/asteronits/Makefile +++ b/contrib/asteronits/Makefile @@ -9,14 +9,10 @@ bin/asteronits: $(shell ${NITLS} -M src/asteronits.nit linux) pre-build bin/texture_atlas_parser: ../../lib/gamnit/texture_atlas_parser.nit ${NITC} ../../lib/gamnit/texture_atlas_parser.nit -o $@ -src/controls.nit: art/controls.svg - make -C ../inkscape_tools/ - ../inkscape_tools/bin/svg_to_png_and_nit art/controls.svg -a assets/ -s src/ -x 2.0 -g - src/spritesheet.nit: bin/texture_atlas_parser bin/texture_atlas_parser art/sheet.xml --dir src/ -n spritesheet -pre-build: src/controls.nit src/spritesheet.nit +pre-build: src/spritesheet.nit check: bin/asteronits NIT_TESTING=true bin/asteronits diff --git a/contrib/asteronits/src/asteronits.nit b/contrib/asteronits/src/asteronits.nit index e5ffedf..41c8657 100644 --- a/contrib/asteronits/src/asteronits.nit +++ b/contrib/asteronits/src/asteronits.nit @@ -230,15 +230,15 @@ redef class KeyEvent # How does this event affect the ship thrust? fun thrust: Float do - if is_arrow_up or name == "w" then return 1.0 + if name == "up" or name == "w" then return 1.0 return 0.0 end # How does this event affect the ship thrust? fun rotation: Float do - if is_arrow_right or name == "d" then return -1.0 - if is_arrow_left or name == "a" then return 1.0 + if name == "right" or name == "d" then return -1.0 + if name == "left" or name == "a" then return 1.0 return 0.0 end end diff --git a/contrib/asteronits/src/touch_ui.nit b/contrib/asteronits/src/touch_ui.nit index 388cd35..bd8bfa5 100644 --- a/contrib/asteronits/src/touch_ui.nit +++ b/contrib/asteronits/src/touch_ui.nit @@ -15,103 +15,20 @@ # Touchscreen UI for mobile devices module touch_ui +import gamnit::virtual_gamepad + import asteronits -import controls redef class App - - # Controls texture - var spritesheet_controls = new ControlsImages - - private var joystick_x = 200.0 - private var joystick_y = 100.0 - - redef fun accept_event(event) - do - super - - var display = display - if display == null then return false - - if event isa PointerEvent and not event.is_move then - - # Convert input coordinates from screen coordinates to UI units. - # Effectively reverting the transformation created by `ui_camera.reset_height`. - var ui_pos = ui_camera.camera_to_ui(event.x, event.y) - - var ship = world.ship - - if ui_pos.y.to_i > display.height/2 then - # Lower half of the screen - if ui_pos.x.to_i > display.width/2 then - # Bottom right - if event.pressed then ship.fire - else - # Bottom left - var dx = ui_pos.x - joystick_x - var dy = ui_pos.y - (ui_camera.height - joystick_y) - - # Any touch in the joystick reset all joystick effects. - # It prevents leaving a button without releasing by moving - # the pointer over another button. - ship.applied_rotation = 0.0 - ship.applied_thrust = 0.0 - - if not event.pressed then return true - - if dy > 0.0 then - # Bottom part of the joystick, turns left or right - if dx < 0.0 then - ship.applied_rotation = 1.0 - else - ship.applied_rotation = -1.0 - end - else - # Upper part of the joystick, detect action using 45d angles - if dx < dy then - ship.applied_rotation = 1.0 - else if dx > -dy then - ship.applied_rotation = -1.0 - else - ship.applied_thrust = 1.0 - end - end - end - end - return true - end - - return false - end - redef fun on_create do super - var display = display - assert display != null - - # Standardize the UI size to look like a 1600 pixels high screen. - # Meaning that the controls have a size proportional to the height of each screen. - # In the code, we can use "pixel" precision as if the target screen was 1600 pixels high. - ui_camera.reset_height 800.0 - - # Add the joystick to the UI - ui_sprites.add new Sprite(spritesheet_controls.forward, - ui_camera.bottom_left.offset(joystick_x, 200.0, 0.0)) - ui_sprites.add new Sprite(spritesheet_controls.left, - ui_camera.bottom_left.offset(joystick_x-100.0, joystick_y, 0.0)) - ui_sprites.add new Sprite(spritesheet_controls.right, - ui_camera.bottom_left.offset(joystick_x+100.0, joystick_y, 0.0)) - - # Purely cosmetic joystick background - ui_sprites.add new Sprite(spritesheet_controls.joystick_back, - ui_camera.bottom_left.offset(joystick_x, joystick_y, -1.0)) # In the back - ui_sprites.add new Sprite(spritesheet_controls.joystick_down, - ui_camera.bottom_left.offset(joystick_x, 0.0, 1.0)) - - # Add the "open fire" button - ui_sprites.add new Sprite(spritesheet_controls.fire, - ui_camera.bottom_right.offset(-150.0, 150.0, 0.0)) + var gamepad = new VirtualGamepad + gamepad.add_dpad + gamepad.controls.first.as(DPad).show_down = false + gamepad.add_button("space", gamepad_spritesheet.fire) + gamepad.visible = true + self.gamepad = gamepad end end diff --git a/lib/android/input_events.nit b/lib/android/input_events.nit index 5ee9d87..96663c7 100644 --- a/lib/android/input_events.nit +++ b/lib/android/input_events.nit @@ -200,7 +200,7 @@ class AndroidPointerEvent motion_event.just_went_down # Unique id of this pointer since the beginning of the gesture - fun pointer_id: Int do return native_pointer_id(motion_event.native, pointer_index) + redef fun pointer_id do return native_pointer_id(motion_event.native, pointer_index) private fun native_pointer_id(motion_event: NativeAndroidMotionEvent, pointer_index: Int): Int `{ return AMotionEvent_getPointerId(motion_event, pointer_index); diff --git a/lib/gamnit/cameras.nit b/lib/gamnit/cameras.nit index be7edcf..401e1ce 100644 --- a/lib/gamnit/cameras.nit +++ b/lib/gamnit/cameras.nit @@ -234,22 +234,34 @@ class UICamera var wx = x.to_f * width / display.width.to_f - position.x var wy = y.to_f * height / display.height.to_f - position.y - return new Point[Float](wx, wy) + return new Point[Float](wx, -wy) end # Center of the screen, from the point of view of the camera, at z = 0 fun center: Point3d[Float] do return new Point3d[Float](position.x + width / 2.0, position.y - height / 2.0, 0.0) - # Anchor in the top left corner of the screen, at z = 0 + # Center of the top of the screen, at z = 0 + fun top: Point3d[Float] do return new Point3d[Float](position.x + width / 2.0, position.y, 0.0) + + # Center of the bottom of the screen, at z = 0 + fun bottom: Point3d[Float] do return new Point3d[Float](position.x + width / 2.0, position.y - height, 0.0) + + # Center of the left border of the screen, at z = 0 + fun left: Point3d[Float] do return new Point3d[Float](position.x, position.y - height / 2.0, 0.0) + + # Center of the right border of the screen, at z = 0 + fun right: Point3d[Float] do return new Point3d[Float](position.x + width, position.y - height / 2.0, 0.0) + + # Top left corner of the screen, at z = 0 fun top_left: Point3d[Float] do return new Point3d[Float](position.x, position.y, 0.0) - # Anchor in the top right corner of the screen, at z = 0 + # Top right corner of the screen, at z = 0 fun top_right: Point3d[Float] do return new Point3d[Float](position.x + width, position.y, 0.0) - # Anchor in the bottom left corner of the screen, at z = 0 + # Bottom left corner of the screen, at z = 0 fun bottom_left: Point3d[Float] do return new Point3d[Float](position.x, position.y - height, 0.0) - # Anchor in the bottom right corner of the screen, at z = 0 + # Bottom right corner of the screen, at z = 0 fun bottom_right: Point3d[Float] do return new Point3d[Float](position.x + width, position.y - height, 0.0) # TODO cache the anchors and the matrix diff --git a/lib/gamnit/flat.nit b/lib/gamnit/flat.nit index d59d7e3..6774f4f 100644 --- a/lib/gamnit/flat.nit +++ b/lib/gamnit/flat.nit @@ -676,6 +676,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 diff --git a/lib/gamnit/keys.nit b/lib/gamnit/keys.nit index 85e9e1e..014d397 100644 --- a/lib/gamnit/keys.nit +++ b/lib/gamnit/keys.nit @@ -18,6 +18,28 @@ # As a `Set`, `app.pressed_keys` can be iterated and queried with `has`. # # Limitations: The keys names are platform dependent. +# +# ~~~nitish +# redef class App +# redef fun accept_event(event) +# do +# # First, pass the event to `super`, `pressed_keys` must see all +# # events but it doesn't intercept any of them. +# if super then return true +# return false +# end +# +# redef fun update(dt) +# do +# for key in pressed_keys do +# if k == "left" or k == "a" then +# # Act on key pressed down +# print "left or a is pressed down" +# end +# end +# end +# end +# ~~~ module keys import mnit::input 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/contrib/asteronits/art/controls.svg b/lib/gamnit/virtual_gamepad/art/virtual_gamepad.svg similarity index 67% rename from contrib/asteronits/art/controls.svg rename to lib/gamnit/virtual_gamepad/art/virtual_gamepad.svg index 903898d..84de166 100644 --- a/contrib/asteronits/art/controls.svg +++ b/lib/gamnit/virtual_gamepad/art/virtual_gamepad.svg @@ -13,7 +13,7 @@ inkscape:version="0.48.5 r10040" width="900" height="592" - sodipodi:docname="controls.svg"> + sodipodi:docname="virtual_gamepad.svg"> @@ -26,13 +26,13 @@ + inkscape:current-layer="svg2" /> - - @@ -1353,15 +1335,6 @@ x2="819.20001" /> - + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - + d="m 658.325,75.40375 0,40.2675 -27.825,27.825 -27.825,-27.825 0,-40.2675 q 0,-1.05 1.05,-1.05 l 53.55,0 q 1.05,0 1.05,1.05" + style="fill:#383838;stroke:none" + inkscape:connector-curvature="0" /> + style="fill:#868383;stroke:none;fill-opacity:1" /> + d="m 707.57,165.725 0,53.55 q 0,1.05 -1.05,1.05 l -39.165,0 -27.825,-27.825 27.825,-27.825 39.165,0 q 1.05,0 1.05,1.05" + style="fill:#383838;stroke:none" + inkscape:connector-curvature="0" /> + style="fill:#868383;stroke:none;fill-opacity:1" /> + d="m 658.325,269.355 0,39.165 q 0,1.05 -1.05,1.05 l -53.55,0 q -1.05,0 -1.05,-1.05 l 0,-39.165 27.825,-27.825 27.825,27.825" + style="fill:#383838;stroke:none" + inkscape:connector-curvature="0" /> + style="fill:#868383;stroke:none;fill-opacity:1" /> + d="m 553.40375,164.675 40.2675,0 27.825,27.825 -27.825,27.825 -40.2675,0 q -1.05,0 -1.05,-1.05 l 0,-53.55 q 0,-1.05 1.05,-1.05" + style="fill:#383838;stroke:none" + inkscape:connector-curvature="0" /> + style="fill:#868383;stroke:none;fill-opacity:1" /> @@ -1907,133 +1818,112 @@ style="fill:#000000;fill-opacity:0.2;stroke:none" /> - - - - + id="path489" + d="m 326.2,232.9 0.3,0.65 -0.3,0.7 -5.05,5 0,0.05 -0.65,0.25 -0.4,-0.05 q -0.6,-0.25 -0.6,-0.9 l 0,-10.05 q 0,-0.65 0.6,-0.9 0.6,-0.25 1.05,0.2 l 5.05,5.05 m -132.4,0 5.05,-5.05 q 0.45,-0.45 1.05,-0.2 0.6,0.25 0.6,0.9 l 0,10.05 q 0,0.65 -0.6,0.9 l -0.4,0.05 -0.65,-0.25 0,-0.05 -5.05,-5 -0.3,-0.7 q 0,-0.35 0.3,-0.65 M 260,167.05 q 0.35,0 0.65,0.3 l 5.05,5.05 q 0.45,0.45 0.2,1.05 -0.25,0.6 -0.9,0.6 l -10.05,0 q -0.65,0 -0.9,-0.6 l -0.05,-0.4 q -0.05,-0.35 0.25,-0.65 l 0.05,0 5,-5.05 q 0.3,-0.3 0.7,-0.3 m 5.9,126.6 q 0.25,0.6 -0.2,1.05 l -5.05,5.05 -0.65,0.3 q -0.4,0 -0.7,-0.3 l -5,-5.05 -0.05,0 q -0.3,-0.3 -0.25,-0.65 l 0.05,-0.4 q 0.25,-0.6 0.9,-0.6 l 10.05,0 q 0.65,0 0.9,0.6" /> + + + + + + + + + + - + id="path371" + d="m 834.5375,346.5375 q 10.5,-10.5525 25.4625,-10.5525 14.9625,0 25.41,10.5525 10.605,10.605 10.605,25.4625 0,14.9625 -10.605,25.41 -10.4475,10.605 -25.41,10.605 -14.9625,0 -25.4625,-10.605 Q 823.985,386.9625 823.985,372 q 0,-14.8575 10.5525,-25.4625" /> + id="path369" + d="M 831.65,343.7 Q 843.35,332 860,332 q 16.55,0 28.25,11.7 Q 900,355.45 900,372 900,388.55 888.25,400.25 876.55,412 860,412 843.35,412 831.65,400.25 820,388.55 820,372 q 0,-16.55 11.65,-28.3 m 4.1,4.05 q -10.05,10.1 -10.05,24.25 0,14.25 10.05,24.2 10,10.1 24.25,10.1 14.25,0 24.2,-10.1 10.1,-9.95 10.1,-24.2 0,-14.15 -10.1,-24.25 -9.95,-10.05 -24.2,-10.05 -14.25,0 -24.25,10.05" /> + id="path4129" + d="m 868.4324,356.58865 c -0.55625,0.0375 -1.025,0.25 -1.375,0.625 l -11.3125,11.34375 c -1.3,-0.83333 -2.66042,-1.38958 -4.09375,-1.65625 l -0.15625,0 -2.4375,-0.0937 c -1.73333,0.16667 -3.20625,0.88958 -4.40625,2.15625 -1.26667,1.16667 -1.93333,2.61042 -2,4.34375 -0.13333,0.76667 -0.10417,1.62917 0.0625,2.5625 l 0,0.0312 c 0.36667,2.4 1.50417,4.56875 3.4375,6.46875 l 0.46875,0.375 c 1.83333,1.7 3.85833,2.75625 6.125,3.15625 2.83333,0.43333 5.14167,-0.26042 6.875,-2.09375 1.8,-1.66667 2.4375,-3.91667 1.9375,-6.75 l 0.0625,-0.0937 c -0.3,-1.53333 -0.85625,-2.97917 -1.65625,-4.3125 l 4.03125,-4.09375 4.21875,4.1875 c 0.83333,0.83333 1.66667,0.83333 2.5,0 l 0.90625,-0.9375 0.125,-0.40625 -0.0937,-0.40625 -0.65625,-0.625 2.25,-2.25 0.65625,0.625 c 0.13333,0.13333 0.27292,0.21875 0.40625,0.21875 l 0.40625,-0.25 0.9375,-0.96875 c 0.83333,-0.8 0.83333,-1.60417 0,-2.4375 l -4.1875,-4.1875 c 0.3,-0.46667 0.43958,-1.02292 0.40625,-1.65625 l -0.125,0 c 0,-0.53333 -0.16667,-1.03542 -0.5,-1.46875 l -0.4375,-0.4375 c -0.5,-0.53333 -1.1125,-0.8375 -1.8125,-0.9375 -0.20833,-0.025 -0.37708,-0.0437 -0.5625,-0.0312 z m -18.34375,16.0625 c 0.1625,0.002 0.35625,0.0292 0.53125,0.0625 1.16667,0.23333 2.25,0.81667 3.25,1.75 l 0.125,0.15625 0.375,0.34375 c 0.76667,0.93333 1.2375,1.90417 1.4375,2.9375 l 0,0.21875 c 0.1,0.63333 -0.0104,1.13125 -0.34375,1.53125 -0.43333,0.4 -0.98958,0.56875 -1.65625,0.46875 l -0.0937,-0.0625 c -1.1,-0.16667 -2.10208,-0.6375 -2.96875,-1.4375 l -0.4375,-0.40625 c -1,-1.03333 -1.61042,-2.14375 -1.84375,-3.34375 -0.1,-0.73333 0.0104,-1.34583 0.34375,-1.8125 0.375,-0.275 0.79375,-0.4125 1.28125,-0.40625 z" + style="fill:#868383;fill-opacity:1;stroke:none" /> + id="path483" + d="m 860,515.985 q 14.9625,0 25.41,10.5525 10.605,10.605 10.605,25.4625 0,14.9625 -10.605,25.41 -10.4475,10.605 -25.41,10.605 -14.9625,0 -25.4625,-10.605 Q 823.985,566.9625 823.985,552 q 0,-14.8575 10.5525,-25.4625 10.5,-10.5525 25.4625,-10.5525" /> - - - - - - - - + - + style="fill:#868383;stroke:none;fill-opacity:1" /> - - - - + + + + + + + + + + + + + + + - + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 diff --git a/lib/mnit/input.nit b/lib/mnit/input.nit index e200d11..e332548 100644 --- a/lib/mnit/input.nit +++ b/lib/mnit/input.nit @@ -41,6 +41,13 @@ interface PointerEvent # Is this a movement event? fun is_move: Bool is abstract + + # Unique identifier of this pointer among other active pointers + # + # This value is useful to differentiate between pointers (or fingers) on + # multi-touch systems. This value does not change for the same pointer + # while it touches the screen. + fun pointer_id: Int do return 0 end # A motion event on screen composed of many `PointerEvent`