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 create_scene
    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

Introduced classes

class DPad

gamnit :: DPad

Directional pad with up to 4 buttons
class RoundButton

gamnit :: RoundButton

Simple action button
abstract class RoundControl

gamnit :: RoundControl

Control composing a VirtualGamepad
class VirtualGamepad

gamnit :: VirtualGamepad

Gamepad on touch screen bound to keyboard keys
class VirtualGamepadEvent

gamnit :: VirtualGamepadEvent

Event fired by a VirtualGamepad

Redefined classes

redef class App

gamnit :: virtual_gamepad $ App

App subclasses are cross-platform applications

All class definitions

redef class App

gamnit :: virtual_gamepad $ App

App subclasses are cross-platform applications
class DPad

gamnit $ DPad

Directional pad with up to 4 buttons
class RoundButton

gamnit $ RoundButton

Simple action button
abstract class RoundControl

gamnit $ RoundControl

Control composing a VirtualGamepad
class VirtualGamepad

gamnit $ VirtualGamepad

Gamepad on touch screen bound to keyboard keys
class VirtualGamepadEvent

gamnit $ VirtualGamepadEvent

Event fired by a VirtualGamepad
package_diagram gamnit::virtual_gamepad virtual_gamepad gamnit\>flat\> flat gamnit::virtual_gamepad->gamnit\>flat\> gamnit::virtual_gamepad_spritesheet virtual_gamepad_spritesheet gamnit::virtual_gamepad->gamnit::virtual_gamepad_spritesheet gamnit gamnit gamnit\>flat\>->gamnit app app gamnit\>flat\>->app gamnit::textures textures gamnit::virtual_gamepad_spritesheet->gamnit::textures ...gamnit ... ...gamnit->gamnit ...app ... ...app->app ...gamnit::textures ... ...gamnit::textures->gamnit::textures a_star-m a_star-m a_star-m->gamnit::virtual_gamepad

Ancestors

module abstract_collection

core :: abstract_collection

Abstract collection classes and services.
module abstract_text

core :: abstract_text

Abstract class for manipulation of sequences of characters
module angles

geometry :: angles

Angle related service using Float to represent an angle in radians
module app

app :: app

app.nit is a framework to create cross-platform applications
module app_base

app :: app_base

Base of the app.nit framework, defines App
module array

core :: array

This module introduces the standard array structure.
module assets

app :: assets

Portable services to load resources from the assets folder
module audio

app :: audio

Services to load and play Sound and Music from the assets folder
module aware

android :: aware

Android compatibility module
module bitset

core :: bitset

Services to handle BitSet
module bmfont

gamnit :: bmfont

Parse Angel Code BMFont format and draw text
module boxes

geometry :: boxes

Provides interfaces and classes to represent basic geometry needs.
module bytes

core :: bytes

Services for byte streams and arrays
module c

c :: c

Structures and services for compatibility with the C language
module caching

serialization :: caching

Services for caching serialization engines
module camera_control

gamnit :: camera_control

Simple camera control for user, as the method accept_scroll_and_zoom
module cameras

gamnit :: cameras

Camera services producing Model-View-Projection matrices
module cameras_cache

gamnit :: cameras_cache

Cache the Matrix produced by Camera::mvp_matrix
module circular_array

core :: circular_array

Efficient data structure to access both end of the sequence.
module codec_base

core :: codec_base

Base for codecs to use with streams
module codecs

core :: codecs

Group module for all codec-related manipulations
module collection

core :: collection

This module define several collection classes.
module core

core :: core

Standard classes and methods used by default by Nit programs and libraries.
module display

gamnit :: display

Abstract display services
module dom

dom :: dom

Easy XML DOM parser
module dynamic_resolution

gamnit :: dynamic_resolution

Virtual screen with a resolution independent from the real screen
module engine_tools

serialization :: engine_tools

Advanced services for serialization engines
module environ

core :: environ

Access to the environment variables of the process
module error

core :: error

Standard error-management infrastructure.
module exec

core :: exec

Invocation and management of operating system sub-processes.
module file

core :: file

File manipulations (create, read, write, etc.)
module fixed_ints

core :: fixed_ints

Basic integers of fixed-precision
module fixed_ints_text

core :: fixed_ints_text

Text services to complement fixed_ints
module flat

core :: flat

All the array-based text representations
module flat_core

gamnit :: flat_core

Core services for the flat API for 2D games
module font

gamnit :: font

Abstract font drawing services, implemented by bmfont and tileset
module gamnit

gamnit :: gamnit

Game and multimedia framework for Nit
module gc

core :: gc

Access to the Nit internal garbage collection mechanism
module geometry

geometry :: geometry

Provides interfaces and classes to represent basic geometry needs.
module glesv2

glesv2 :: glesv2

OpenGL graphics rendering library for embedded systems, version 2.0
module hash_collection

core :: hash_collection

Introduce HashMap and HashSet.
module input

mnit :: input

Defines abstract classes for user and general inputs to the application.
module inspect

serialization :: inspect

Refine Serializable::inspect to show more useful information
module iso8859_1

core :: iso8859_1

Codec for ISO8859-1 I/O
module kernel

core :: kernel

Most basic classes and methods.
module keys

gamnit :: keys

Simple service keeping track of which keys are currently pressed
module limit_fps

gamnit :: limit_fps

Frame-rate control for applications
module list

core :: list

This module handle double linked lists
module math

core :: math

Mathematical operations
module matrix

matrix :: matrix

Services for matrices of Float values
module meta

meta :: meta

Simple user-defined meta-level to manipulate types of instances as object.
module more_collections

more_collections :: more_collections

Highly specific, but useful, collections-related classes.
module native

core :: native

Native structures for text and bytes
module numeric

core :: numeric

Advanced services for Numeric types
module parser

dom :: parser

XML DOM-parsing facilities
module parser_base

parser_base :: parser_base

Simple base for hand-made parsers of all kinds
module performance_analysis

performance_analysis :: performance_analysis

Services to gather information on the performance of events by categories
module points_and_lines

geometry :: points_and_lines

Interfaces and classes to represent basic geometry needs.
module poset

poset :: poset

Pre order sets and partial order set (ie hierarchies)
module programs

gamnit :: programs

Services for graphical programs with shaders, attributes and uniforms
module projection

matrix :: projection

Services on Matrix to transform and project 3D coordinates
module protocol

core :: protocol

module queue

core :: queue

Queuing data structures and wrappers
module range

core :: range

Module for range of discrete objects.
module re

core :: re

Regular expression support for all services based on Pattern
module realtime

realtime :: realtime

Services to keep time of the wall clock time
module ropes

core :: ropes

Tree-based representation of a String.
module serialization

serialization :: serialization

General serialization services
module serialization_core

serialization :: serialization_core

Abstract services to serialize Nit objects to different formats
module sorter

core :: sorter

This module contains classes used to compare things and sorts arrays.
module stream

core :: stream

Input and output streams of characters
module text

core :: text

All the classes and methods related to the manipulation of text entities
module textures

gamnit :: textures

Load textures, create subtextures and manage their life-cycle
module tileset

gamnit :: tileset

Support for TileSet, TileSetFont and drawing text with TextSprites
module time

core :: time

Management of time and dates
module union_find

core :: union_find

union–find algorithm using an efficient disjoint-set data structure
module utf8

core :: utf8

Codec for UTF-8 I/O
module xml_entities

dom :: xml_entities

Basic blocks for DOM-XML representation

Parents

module flat

gamnit :: flat

Simple API for 2D games, built around Sprite and App::update

Children

module a_star-m

a_star-m

# 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 create_scene
#     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 is writable

	# 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 and return 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]): nullable DPad
	do
		if names == null then names = ["w","a","s","d"]
		assert names.length == 4

		if n_dpads == 0 then
			var dpad = new DPad(app.ui_camera.bottom_left.offset(200.0, 100.0, 0.0), names)
			controls.add dpad
			return dpad
		else if n_dpads == 1 then
			var dpad = new DPad(app.ui_camera.bottom_right.offset(-200.0, 100.0, 0.0), names)
			controls.add dpad
			return dpad
		else
			print_error "Too many DPad ({n_dpads}) in {self}"
			return null
		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 and return 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): nullable RoundButton
	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
		var but = new RoundButton(
			app.ui_camera.bottom_right.offset(pos.x, pos.y, 0.0), name, texture)
		controls.add but
		return but
	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
		var back = new Sprite(app.gamepad_spritesheet.joystick_back,
			center.offset(0.0, 0.0+dy, -1.0)) # In the back
		back.draw_order = -1
		sprites.add back

		# Non-interactive handle in the bottom
		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
lib/gamnit/virtual_gamepad/virtual_gamepad.nit:15,1--425,3