1 # This file is part of NIT ( http://www.nitlanguage.org ).
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
7 # http://www.apache.org/licenses/LICENSE-2.0
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
15 # Virtual gamepad mapped to keyboard keys for quick and dirty mobile support
17 # For Android, the texture is automatically added to the APK when this
18 # module is imported. However, due to a limitation of the _app.nit_
19 # framework on desktop OS, the texture must be copied manually to the assets
20 # folder at `assets/images`, the texture is available at, from the repo root,
21 # `lib/gamnit/virtual_gamepad/assets/images`.
23 # The texture was created by kenney.nl and modified by Alexis Laferrière.
24 # It is published under CC0 and can be used and modified without attribution.
28 # redef fun create_scene
32 # # Create the virtual gamepad
33 # var gamepad = new VirtualGamepad
35 # # Configure it as needed
36 # gamepad.add_dpad(["w","a","s","d"])
37 # gamepad.add_button("x", gamepad_spritesheet.x)
38 # gamepad.add_button("space", gamepad_spritesheet.star)
40 # # Assign it as the active gamepad
41 # self.gamepad = gamepad
46 # # Show the virtual gamepad
47 # var gamepad = self.gamepad
48 # if gamepad != null then gamepad.visible = true
52 module virtual_gamepad
is app_files
56 import virtual_gamepad_spritesheet
60 # Current touch gamepad, still may be invisible
61 var gamepad
: nullable VirtualGamepad = null is writable
63 # Textures used for `DPad` and available to clients
64 var gamepad_spritesheet
= new VirtualGamepadSpritesheet
66 redef fun accept_event
(event
)
68 # Priority to the gamepad
70 if gamepad
!= null and gamepad
.accept_event
(event
) then return true
76 # Gamepad on touch screen bound to keyboard keys
78 # Fires `VirtualGamepadEvent` which implement `KeyEvent` so it behaves like a keyboard.
81 private var sprites
= new Array[Sprite]
83 # Controls composing this gamepad
85 # Controls can be added directly to this array or using `add_dpad`
87 var controls
= new Array[RoundControl]
89 # Add and return a directional pad (`DPad`) to a default location
91 # The 4 buttons fire events with the corresponding name in `names`.
92 # Items in `names` should be in order of top, left, down and right.
93 # If `null`, defaults to WASD.
95 # If this method is called, it should be before `add_button` to
96 # avoid overlapping controls.
98 # A maximum of 2 `DPad` may be added using this method.
99 # The first `DPad` is placed on the left of the screen.
100 # The second `DPad` is on the right and replaces some buttons
101 # added by `add_button`.
103 # Require: `names == null or names.length == 4`
104 fun add_dpad
(names
: nullable Array[String]): nullable DPad
106 if names
== null then names
= ["w","a","s","d"]
107 assert names
.length
== 4
110 var dpad
= new DPad(app
.ui_camera
.bottom_left
.offset
(200.0, 100.0, 0.0), names
)
113 else if n_dpads
== 1 then
114 var dpad
= new DPad(app
.ui_camera
.bottom_right
.offset
(-200.0, 100.0, 0.0), names
)
118 print_error
"Too many DPad ({n_dpads}) in {self}"
123 # Number of `DPad` in `controls`
124 private fun n_dpads
: Int
127 for c
in controls
do if c
isa DPad then n_dpads
+= 1
131 # Button positions for `add_button`, offsets from the bottom right
132 private var button_positions
= new Array[Point[Float]].with_items
(
133 new Point[Float](-150.0, 150.0),
134 new Point[Float](-150.0, 350.0),
135 new Point[Float](-150.0, 550.0),
136 new Point[Float](-350.0, 150.0),
137 new Point[Float](-350.0, 350.0),
138 new Point[Float](-350.0, 550.0))
140 # Add and return a round button to a default location
142 # Fired events use `name`, it should usually correspond to a
143 # keyboard key like "space" or "a".
144 # `texture` is displayed at the button position, it also sets the
145 # touchable surface of the button.
147 # If this method is called, it should be after `add_dpad` to
148 # avoid overlapping controls.
150 # A maximum of 6 buttons may be added using this method when
151 # there is less than 2 `DPad`. Otherwise, only 2 buttons can be added.
152 fun add_button
(name
: String, texture
: Texture): nullable RoundButton
154 if n_dpads
== 2 and button_positions
.length
== 6 then
155 # Drop the bottom 4 buttons
156 button_positions
.remove_at
4
157 button_positions
.remove_at
3
158 button_positions
.remove_at
1
159 button_positions
.remove_at
0
162 assert button_positions
.not_empty
else print_error
"Too many buttons in {self}"
163 var pos
= button_positions
.shift
164 var but
= new RoundButton(
165 app
.ui_camera
.bottom_right
.offset
(pos
.x
, pos
.y
, 0.0), name
, texture
)
172 var display
= app
.display
173 assert display
!= null
175 for control
in controls
do
176 var sprites
= control
.sprites
177 app
.ui_sprites
.add_all sprites
181 # Is this control visible?
182 var visible
= false is private writable(visible_direct
=)
184 # Set this control to visible or not
185 fun visible
=(value
: Bool)
187 visible_direct
= value
188 if value
then show
else hide
193 if sprites
.is_empty
then prepare
194 app
.ui_sprites
.add_all sprites
197 private fun hide
do for s
in sprites
do app
.ui_sprites
.remove_all s
199 private var control_under_pointer
= new Map[Int, RoundControl]
201 private fun accept_event
(event
: InputEvent): Bool
203 if not visible
then return false
205 var display
= app
.display
206 if display
== null then return false
208 if event
isa PointerEvent then
209 var ui_pos
= app
.ui_camera
.camera_to_ui
(event
.x
, event
.y
)
211 for control
in controls
do
212 if control
.accept_event
(event
, ui_pos
) then
213 var prev_control
= control_under_pointer
.get_or_null
(event
.pointer_id
)
214 if prev_control
!= null and prev_control
!= control
then
215 prev_control
.depressed_down
217 control_under_pointer
[event
.pointer_id
] = control
222 var prev_control
= control_under_pointer
.get_or_null
(event
.pointer_id
)
223 if prev_control
!= null then prev_control
.depressed_down
224 control_under_pointer
.keys
.remove event
.pointer_id
231 # Event fired by a `VirtualGamepad`
232 class VirtualGamepadEvent
237 redef var is_down
is noautoinit
240 # Control composing a `VirtualGamepad`
241 abstract class RoundControl
242 # Center position on the UI
243 var center
: Point3d[Float]
245 # Radius in UI units/pixels of the part of this control
246 fun radius
: Float is abstract
248 private fun sprites
: Array[Sprite] do return new Array[Sprite]
250 private fun accept_event
(event
: InputEvent, ui_pos
: Point[Float]): Bool
252 if event
isa PointerEvent and contains
(ui_pos
) then
253 return hit
(event
, ui_pos
)
259 # Does `self` contain a pointer at `ui_pos`?
260 private fun contains
(ui_pos
: Point[Float]): Bool
262 var dx
= center
.x
- ui_pos
.x
263 var dy
= center
.y
- ui_pos
.y
264 return dx
*dx
+ dy
*dy
< radius
*radius
267 private fun hit
(event
: PointerEvent, ui_pos
: Point[Float]): Bool
270 # Keys currently down, to be depressed if the pointer moves away
271 private var down_names
= new Set[String]
273 # Depress/release keys kept down, listed by `down_names`
274 private fun depressed_down
276 for name
in down_names
do
277 var e
= new VirtualGamepadEvent(name
)
285 # Simple action button
289 # Event name, should usually correspond to a keyboard key like "a" or "left"
292 # Texture drawn for this button, may be from `app.gamepad_spritesheet`
295 redef fun radius
do return 0.5*texture
.height
297 redef fun hit
(event
, ui_pos
)
299 if not event
.is_move
then
300 var e
= new VirtualGamepadEvent(name
.to_s
)
301 e
.is_down
= event
.pressed
304 if event
.pressed
then
306 else down_names
.clear
311 redef var sprites
= [new Sprite(texture
, center
)] is lazy
314 # Directional pad with up to 4 buttons
316 # Assumes that each pad is manipulated by at max a single pointer.
320 # Event names for the keys, in order of top, left, down and right
321 var names
: Array[String]
324 var show_up
= true is writable
326 # Show the down button
327 var show_down
= true is writable
329 # Show the left button
330 var show_left
= true is writable
332 # Show the right button
333 var show_right
= true is writable
335 redef fun radius
do return 200.0
337 redef fun contains
(ui_pos
)
339 if show_down
then return super(new Point[Float](ui_pos
.x
+0.0, ui_pos
.y-100
.0
))
343 redef fun hit
(event
, ui_pos
)
345 var display
= app
.display
346 if display
== null then return false
348 var dx
= ui_pos
.x
- center
.x
349 var dy
= ui_pos
.y
- center
.y
350 if show_down
then dy
-= 100.0
352 # Angle (with 0.0 on the right) to index in WASD (0 -> W/up)
353 var indexes
= new Set[Int]
354 var ao
= atan2
(dy
, dx
)
357 # Look for 2 angles so 2 buttons can be pressed at the same time
358 for da
in once
[-pi
/8.0, pi
/8.0] do
360 while a
< 0.0 do a
+= pi
*2.0
361 while a
> 2.0*pi
do a
-= pi
*2.0
362 var index
= (a
* 2.0 / pi
).floor
.to_i
363 if index
< 0 then index
+= 4
367 var shows
= [show_up
, show_left
, show_down
, show_right
]
368 var new_down_names
= new Set[String]
369 for index
in indexes
do
370 # Don't trigger events for hidden buttons/directions
371 if not shows
[index
] then continue
373 var name
= names
[index
]
375 var e
= new VirtualGamepadEvent(name
)
376 e
.is_down
= event
.pressed
379 if event
.pressed
then new_down_names
.add name
382 # Depress released directions
383 for name
in down_names
do
384 if not new_down_names
.has
(name
) then
385 var e
= new VirtualGamepadEvent(name
)
391 down_names
= new_down_names
399 if show_down
then dy
= 100.0
401 var sprites
= new Array[Sprite]
403 # Interactive buttons
404 if show_up
then sprites
.add
new Sprite(app
.gamepad_spritesheet
.dpad_up
,
405 center
.offset
( 0.0, 100.0+dy
, 0.0))
406 if show_left
then sprites
.add
new Sprite(app
.gamepad_spritesheet
.dpad_left
,
407 center
.offset
(-100.0, 0.0+dy
, 0.0))
408 if show_right
then sprites
.add
new Sprite(app
.gamepad_spritesheet
.dpad_right
,
409 center
.offset
( 100.0, 0.0+dy
, 0.0))
410 if show_down
then sprites
.add
new Sprite(app
.gamepad_spritesheet
.dpad_down
,
411 center
.offset
( 0.0,-100.0+dy
, 0.0))
413 # Non-interactive joystick background
414 var back
= new Sprite(app
.gamepad_spritesheet
.joystick_back
,
415 center
.offset
(0.0, 0.0+dy
, -1.0)) # In the back
419 # Non-interactive handle in the bottom
420 if not show_down
then sprites
.add
new Sprite(app
.gamepad_spritesheet
.joystick_down
,
421 center
.offset
(0.0, -100.0+dy
, 0.0))