diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/GraphicsSystem.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/GraphicsSystem.groovy index ffc4d830..8cd29159 100644 --- a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/GraphicsSystem.groovy +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/GraphicsSystem.groovy @@ -25,6 +25,7 @@ import nz.net.ultraq.redhorizon.engine.graphics.opengl.OpenGLContext import nz.net.ultraq.redhorizon.engine.graphics.opengl.OpenGLRenderer import nz.net.ultraq.redhorizon.engine.graphics.opengl.OpenGLWindow import nz.net.ultraq.redhorizon.engine.graphics.pipeline.RenderPipeline +import nz.net.ultraq.redhorizon.engine.input.GamepadStateProcessor import nz.net.ultraq.redhorizon.engine.input.InputEventStream import nz.net.ultraq.redhorizon.engine.input.KeyEvent import nz.net.ultraq.redhorizon.engine.input.MouseButtonEvent @@ -57,7 +58,6 @@ class GraphicsSystem extends EngineSystem implements GraphicsRequests { private OpenGLRenderer renderer private ImGuiLayer imGuiLayer private RenderPipeline renderPipeline - private boolean shouldToggleFullScreen private boolean shouldToggleVsync private long lastClickTime @@ -184,6 +184,8 @@ class GraphicsSystem extends EngineSystem implements GraphicsRequests { Thread.currentThread().name = 'Graphics System' logger.debug('Starting graphics system') + var gamepadStateProcessor = new GamepadStateProcessor(inputEventStream) + // Initialization new OpenGLContext(windowTitle, config).withCloseable { context -> window = context.window @@ -237,6 +239,7 @@ class GraphicsSystem extends EngineSystem implements GraphicsRequests { pipeline.render() window.swapBuffers() window.pollEvents() + gamepadStateProcessor.process() } catch (InterruptedException ignored) { break diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/imgui/ImGuiLoggingAppender.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/imgui/ImGuiLoggingAppender.groovy index c2bdde7f..5775ada3 100644 --- a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/imgui/ImGuiLoggingAppender.groovy +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/imgui/ImGuiLoggingAppender.groovy @@ -1,12 +1,12 @@ -/* +/* * Copyright 2021, Emanuel Rabina (http://www.ultraq.net.nz/) - * + * * 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. @@ -24,7 +24,7 @@ import ch.qos.logback.core.encoder.Encoder /** * A custom logback appender made for moving logged events to the debug overlay * created using ImGui. - * + * * @author Emanuel Rabina */ class ImGuiLoggingAppender extends UnsynchronizedAppenderBase implements EventTarget { @@ -45,13 +45,19 @@ class ImGuiLoggingAppender extends UnsynchronizedAppenderBase implements E @Override protected void append(E eventObject) { - def message = new String(encoder.encode(eventObject)) + var message = new String(encoder.encode(eventObject)) if (eventObject.message.contains('average time')) { trigger(new ImGuiLogEvent( message: message, persistentKey: eventObject.argumentArray[0] )) } + else if (eventObject.loggerName.contains('GamepadStateProcessor')) { + trigger(new ImGuiLogEvent( + message: message, + persistentKey: eventObject.message + )) + } else { trigger(new ImGuiLogEvent( message: message diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/input/GamepadAxisEvent.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/input/GamepadAxisEvent.groovy new file mode 100644 index 00000000..8855df7b --- /dev/null +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/input/GamepadAxisEvent.groovy @@ -0,0 +1,31 @@ +/* + * Copyright 2024, Emanuel Rabina (http://www.ultraq.net.nz/) + * + * 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. + */ + +package nz.net.ultraq.redhorizon.engine.input + +import groovy.transform.TupleConstructor + +/** + * Event emitted for gamepad axis controls. + * + * @author Emanuel Rabina + */ +@TupleConstructor(defaults = false) +class GamepadAxisEvent extends InputEvent { + + final int type + final float value +} diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/input/GamepadControl.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/input/GamepadControl.groovy new file mode 100644 index 00000000..14a1c233 --- /dev/null +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/input/GamepadControl.groovy @@ -0,0 +1,65 @@ +/* + * Copyright 2024, Emanuel Rabina (http://www.ultraq.net.nz/) + * + * 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. + */ + +package nz.net.ultraq.redhorizon.engine.input + +import static org.lwjgl.glfw.GLFW.GLFW_GAMEPAD_AXIS_LEFT_X +import static org.lwjgl.glfw.GLFW.GLFW_GAMEPAD_AXIS_LEFT_Y + +import groovy.transform.stc.ClosureParams +import groovy.transform.stc.SimpleType + +/** + * Gamepad-specific control class. + * + * @author Emanuel Rabina + */ +class GamepadControl extends Control { + + private final int type + private final Closure handler + + /** + * Create a new gamepad control to act on the given gamepad axis events. + */ + GamepadControl(int type, String name, + @ClosureParams(value = SimpleType, options = 'float') Closure handler) { + + super(GamepadAxisEvent, name, determineBindingName(type)) + this.type = type + this.handler = handler + } + + /** + * Return a string representation of the axis binding. + */ + private static String determineBindingName(int type) { + + return switch (type) { + case GLFW_GAMEPAD_AXIS_LEFT_X -> 'Left stick X axis' + case GLFW_GAMEPAD_AXIS_LEFT_Y -> 'Left stick Y axis' + default -> '(unknown)' + } + } + + @Override + void handleEvent(GamepadAxisEvent event) { + + if (event.type == type) { + handler(event.value) + } + } +} diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/input/GamepadStateProcessor.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/input/GamepadStateProcessor.groovy new file mode 100644 index 00000000..07a7aecb --- /dev/null +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/input/GamepadStateProcessor.groovy @@ -0,0 +1,78 @@ +/* + * Copyright 2024, Emanuel Rabina (http://www.ultraq.net.nz/) + * + * 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. + */ + +package nz.net.ultraq.redhorizon.engine.input + +import nz.net.ultraq.redhorizon.engine.graphics.GraphicsSystem + +import org.lwjgl.glfw.GLFWGamepadState +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import static org.lwjgl.glfw.GLFW.* + +import groovy.transform.TupleConstructor +import java.nio.FloatBuffer + +/** + * A class for managing gamepad inputs and emitting them as events so that it + * all acts the same for those on the other end of the {@link InputEventStream}. + *

+ * GLFW currently doesn't have a callback system in place for joysticks, and so + * you have to do DIY polling and handling. See https://github.com/glfw/glfw/issues/601, + * which is unlikely to be solved any time soon. + *

+ * Joystick/Gamepad processing is currently restricted to the same thread as the + * GLFW context, so this class is only used from the {@link GraphicsSystem}. + * + * @author Emanuel Rabina + */ +@TupleConstructor(defaults = false) +class GamepadStateProcessor { + + private static final Logger logger = LoggerFactory.getLogger(GamepadStateProcessor) + + final InputEventStream inputEventStream + + @Lazy + private GLFWGamepadState gamepadState = { GLFWGamepadState.create() }() + + /** + * Check for any changes to the joystick/gamepad state and emit events for + * them. Called after {@code glfwPollEvents}. + */ + void process() { + + if (glfwJoystickIsGamepad(GLFW_JOYSTICK_1)) { + glfwGetGamepadState(GLFW_JOYSTICK_1, gamepadState) + + var axes = gamepadState.axes() + processAxis(axes, GLFW_GAMEPAD_AXIS_LEFT_X, 'Gamepad left stick X: {}') + processAxis(axes, GLFW_GAMEPAD_AXIS_LEFT_Y, 'Gamepad left stick Y: {}') + processAxis(axes, GLFW_GAMEPAD_AXIS_RIGHT_X, 'Gamepad right stick X: {}') + processAxis(axes, GLFW_GAMEPAD_AXIS_RIGHT_Y, 'Gamepad right stick Y: {}') + } + } + + /** + * Process a single axis. + */ + private void processAxis(FloatBuffer axes, int type, String logMessage) { + + var value = axes.get(type) + logger.debug(logMessage, sprintf('%.2f', value)) + inputEventStream.trigger(new GamepadAxisEvent(type, value <= -0.2f || 0.2f <= value ? value : 0f)) + } +} diff --git a/redhorizon-shooter/source/nz/net/ultraq/redhorizon/shooter/objects/Player.groovy b/redhorizon-shooter/source/nz/net/ultraq/redhorizon/shooter/objects/Player.groovy index 6361184c..69613c9c 100644 --- a/redhorizon-shooter/source/nz/net/ultraq/redhorizon/shooter/objects/Player.groovy +++ b/redhorizon-shooter/source/nz/net/ultraq/redhorizon/shooter/objects/Player.groovy @@ -21,7 +21,7 @@ import nz.net.ultraq.redhorizon.classic.nodes.Rotatable import nz.net.ultraq.redhorizon.classic.units.Unit import nz.net.ultraq.redhorizon.classic.units.UnitData import nz.net.ultraq.redhorizon.engine.graphics.GraphicsRenderer -import nz.net.ultraq.redhorizon.engine.input.KeyControl +import nz.net.ultraq.redhorizon.engine.input.GamepadControl import nz.net.ultraq.redhorizon.engine.resources.ResourceManager import nz.net.ultraq.redhorizon.engine.scenegraph.GraphicsElement import nz.net.ultraq.redhorizon.engine.scenegraph.Node @@ -30,7 +30,7 @@ import nz.net.ultraq.redhorizon.engine.scenegraph.Temporal import nz.net.ultraq.redhorizon.engine.scenegraph.scripting.Script import nz.net.ultraq.redhorizon.shooter.Shooter -import org.joml.Vector3f +import org.joml.Vector2f import static org.lwjgl.glfw.GLFW.* import groovy.json.JsonSlurper @@ -46,14 +46,16 @@ class Player extends Node implements GraphicsElement, Rotatable, Tempora private static final float FORWARD_SPEED = 100f private static final float ROTATION_SPEED = 120f private static final float STRAFING_SPEED = 50f + private static final Vector2f up = new Vector2f(0, 1) private final Unit unit private final float xPosRange private final float yPosRange - private final Vector3f velocity = new Vector3f() - private float forward = 0f - private float strafing = 0f - private float rotation = 0f + private final Vector2f velocity = new Vector2f() + private final Vector2f direction = new Vector2f() +// private float forward = 0f +// private float strafing = 0f +// private float rotation = 0f private long moveUpdateTimeMs /** @@ -106,15 +108,28 @@ class Player extends Node implements GraphicsElement, Rotatable, Tempora var moveCurrentTimeMs = currentTimeMs var frameDelta = (moveCurrentTimeMs - moveUpdateTimeMs) / 1000 - if (forward || strafing || rotation) { - heading += rotation * frameDelta - var v = velocity.set(strafing, forward, 0).mul(frameDelta).rotateZ(Math.toRadians(-heading) as float) + // Keyboard action +// if (forward || strafing || rotation) { +// heading += rotation * frameDelta +// var v = velocity.set(strafing, forward, 0).mul(frameDelta).rotateZ(Math.toRadians(-heading) as float) +// var currentPosition = getPosition() +// setPosition( +// Math.clamp(currentPosition.x + v.x as float, -xPosRange, xPosRange), +// Math.clamp(currentPosition.y + v.y as float, -yPosRange, yPosRange) +// ) +// } + // Gamepad action + if (velocity.length()) { var currentPosition = getPosition() + var v = new Vector2f(velocity).mul(frameDelta) setPosition( Math.clamp(currentPosition.x + v.x as float, -xPosRange, xPosRange), Math.clamp(currentPosition.y + v.y as float, -yPosRange, yPosRange) ) } + if (direction.length()) { + heading = Math.toDegrees(direction.angle(up)) as float + } moveUpdateTimeMs = moveCurrentTimeMs } @@ -142,61 +157,78 @@ class Player extends Node implements GraphicsElement, Rotatable, Tempora } // TODO: Inertia and momentum + // Keyboard controls +// scene.inputEventStream.addControls( +// new KeyControl(GLFW_KEY_W, 'Move forward', +// { -> +// forward += FORWARD_SPEED +// startMovement() +// }, +// { -> +// forward -= FORWARD_SPEED +// } +// ), +// new KeyControl(GLFW_KEY_S, 'Move backward', +// { -> +// forward -= FORWARD_SPEED +// startMovement() +// }, +// { -> +// forward += FORWARD_SPEED +// } +// ), +// new KeyControl(GLFW_KEY_A, 'Move left', +// { -> +// strafing -= STRAFING_SPEED +// startMovement() +// }, +// { -> +// strafing += STRAFING_SPEED +// } +// ), +// new KeyControl(GLFW_KEY_D, 'Move right', +// { -> +// strafing += STRAFING_SPEED +// startMovement() +// }, +// { -> +// strafing -= STRAFING_SPEED +// } +// ), +// new KeyControl(GLFW_KEY_LEFT, 'Rotate left', +// { -> +// rotation -= ROTATION_SPEED +// startMovement() +// }, +// { -> +// rotation += ROTATION_SPEED +// } +// ), +// new KeyControl(GLFW_KEY_RIGHT, 'Rotate right', +// { -> +// rotation += ROTATION_SPEED +// startMovement() +// }, +// { -> +// rotation -= ROTATION_SPEED +// } +// ) +// ) + + // Gamepad controls scene.inputEventStream.addControls( - new KeyControl(GLFW_KEY_W, 'Move forward', - { -> - forward += FORWARD_SPEED - startMovement() - }, - { -> - forward -= FORWARD_SPEED - } - ), - new KeyControl(GLFW_KEY_S, 'Move backward', - { -> - forward -= FORWARD_SPEED - startMovement() - }, - { -> - forward += FORWARD_SPEED - } - ), - new KeyControl(GLFW_KEY_A, 'Move left', - { -> - strafing -= STRAFING_SPEED - startMovement() - }, - { -> - strafing += STRAFING_SPEED - } - ), - new KeyControl(GLFW_KEY_D, 'Move right', - { -> - strafing += STRAFING_SPEED - startMovement() - }, - { -> - strafing -= STRAFING_SPEED - } - ), - new KeyControl(GLFW_KEY_LEFT, 'Rotate left', - { -> - rotation -= ROTATION_SPEED - startMovement() - }, - { -> - rotation += ROTATION_SPEED - } - ), - new KeyControl(GLFW_KEY_RIGHT, 'Rotate right', - { -> - rotation += ROTATION_SPEED - startMovement() - }, - { -> - rotation -= ROTATION_SPEED - } - ) + new GamepadControl(GLFW_GAMEPAD_AXIS_LEFT_X, 'Movement along the X axis', { value -> + velocity.x = value * FORWARD_SPEED + }), + new GamepadControl(GLFW_GAMEPAD_AXIS_LEFT_Y, 'Movement along the Y axis', { value -> + velocity.y = -value * FORWARD_SPEED + }), + new GamepadControl(GLFW_GAMEPAD_AXIS_RIGHT_X, 'Heading along the X axis', { value -> + direction.x = value + }), + new GamepadControl(GLFW_GAMEPAD_AXIS_RIGHT_Y, 'Heading along the Y axis', { value -> + direction.y = -value + }) ) }