From 9e65f938abfbf6f586c0dec65497a64a10af59d5 Mon Sep 17 00:00:00 2001 From: Emanuel Rabina Date: Sun, 2 Feb 2025 23:00:03 +1300 Subject: [PATCH] WIP --- .../engine/input/GamepadControl.groovy | 28 ++++++- .../ultraq/redhorizon/shooter/Shooter.groovy | 8 +- .../redhorizon/shooter/objects/Bullet.groovy | 71 +++++++++++++++++ .../redhorizon/shooter/objects/Player.groovy | 78 +++++++++---------- 4 files changed, 138 insertions(+), 47 deletions(-) create mode 100644 redhorizon-shooter/source/nz/net/ultraq/redhorizon/shooter/objects/Bullet.groovy 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 index 604ab0a2..3d740cc5 100644 --- a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/input/GamepadControl.groovy +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/input/GamepadControl.groovy @@ -28,18 +28,31 @@ import groovy.transform.stc.SimpleType */ class GamepadControl extends Control { + private static final int[] AXIS_TYPES = [ + GLFW_GAMEPAD_AXIS_LEFT_X, GLFW_GAMEPAD_AXIS_LEFT_Y, + GLFW_GAMEPAD_AXIS_RIGHT_X, GLFW_GAMEPAD_AXIS_RIGHT_Y + ] + private static final float AXIS_THRESHOLD = 0.2f // TODO: Configurable deadzone + + private static final int[] TRIGGER_TYPES = [ + GLFW_GAMEPAD_AXIS_LEFT_TRIGGER, + GLFW_GAMEPAD_AXIS_RIGHT_TRIGGER + ] + private final int type private final Closure handler + private final Closure releaseHandler /** * 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) { + @ClosureParams(value = SimpleType, options = 'float') Closure handler, Closure releaseHandler = null) { super(GamepadAxisEvent, name, determineBindingName(type)) this.type = type this.handler = handler + this.releaseHandler = releaseHandler } /** @@ -61,7 +74,18 @@ class GamepadControl extends Control { void handleEvent(GamepadAxisEvent event) { if (event.type == type) { - handler(event.value) + if (type in AXIS_TYPES) { + var value = event.value + if (Math.abs(value) > AXIS_THRESHOLD) { + handler(event.value) + } + else { + releaseHandler?.call() + } + } + else { + handler(event.value) + } } } } diff --git a/redhorizon-shooter/source/nz/net/ultraq/redhorizon/shooter/Shooter.groovy b/redhorizon-shooter/source/nz/net/ultraq/redhorizon/shooter/Shooter.groovy index 124841f3..3b38139c 100644 --- a/redhorizon-shooter/source/nz/net/ultraq/redhorizon/shooter/Shooter.groovy +++ b/redhorizon-shooter/source/nz/net/ultraq/redhorizon/shooter/Shooter.groovy @@ -16,8 +16,6 @@ package nz.net.ultraq.redhorizon.shooter -import nz.net.ultraq.redhorizon.classic.filetypes.IniFile -import nz.net.ultraq.redhorizon.classic.filetypes.MapFile import nz.net.ultraq.redhorizon.classic.filetypes.PalFile import nz.net.ultraq.redhorizon.classic.maps.Map import nz.net.ultraq.redhorizon.classic.nodes.GlobalPalette @@ -93,9 +91,9 @@ class Shooter { camera.follow(player) - var mapFile = resourceManager.loadFile('scm01ea.ini', IniFile) - var map = new Map(mapFile as MapFile, resourceManager) - scene << map +// var mapFile = resourceManager.loadFile('scm01ea.ini', IniFile) +// var map = new Map(mapFile as MapFile, resourceManager) +// scene << map } } diff --git a/redhorizon-shooter/source/nz/net/ultraq/redhorizon/shooter/objects/Bullet.groovy b/redhorizon-shooter/source/nz/net/ultraq/redhorizon/shooter/objects/Bullet.groovy new file mode 100644 index 00000000..3e3cefc3 --- /dev/null +++ b/redhorizon-shooter/source/nz/net/ultraq/redhorizon/shooter/objects/Bullet.groovy @@ -0,0 +1,71 @@ +/* + * Copyright 2025, 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.shooter.objects + +import nz.net.ultraq.redhorizon.classic.nodes.PalettedSprite +import nz.net.ultraq.redhorizon.classic.nodes.Rotatable +import nz.net.ultraq.redhorizon.engine.scenegraph.Temporal +import nz.net.ultraq.redhorizon.filetypes.ImagesFile + +import org.joml.Vector2f + +/** + * A projectile fired by the player unit. + * + * @author Emanuel Rabina + */ +class Bullet extends PalettedSprite implements Rotatable, Temporal { + + private static final float MOVEMENT_SPEED = 100 + + // TODO: Load once, create instances + + private final Vector2f velocity // TODO: Velocity = ECS component candidate + private final Vector2f movement = new Vector2f() + + /** + * Constructor, load the sprite for the bullet. + */ + Bullet(ImagesFile imagesFile, float heading) { + + super(imagesFile) + bounds { -> + setMax(imagesFile.width, imagesFile.height) + } + + var headings = 32 // "dragon" sprite has 32 headings + var degreesPerHeading = 360f / headings + + // NOTE: C&C unit headings were ordered in a counter-clockwise order + // (maybe to match how radians work?), the reverse from how + // degrees-based headings are done. + var closestHeading = Math.round(heading / degreesPerHeading) + frame = closestHeading ? headings - closestHeading as int : 0 + + var headingInRadians = Math.toRadians(heading) + velocity = new Vector2f((float)Math.sin(headingInRadians), (float)Math.cos(headingInRadians)).normalize() + } + + @Override + void update(float delta) { + + movement.set(velocity).normalize().mul(MOVEMENT_SPEED).mul(delta).add(position.x(), position.y()) + setPosition(movement.x, movement.y) + + super.update(delta) + } +} 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 f54d53db..ccd916b7 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 @@ -32,6 +32,7 @@ import nz.net.ultraq.redhorizon.engine.scenegraph.Scene import nz.net.ultraq.redhorizon.engine.scenegraph.Temporal import nz.net.ultraq.redhorizon.engine.scenegraph.UpdateHint import nz.net.ultraq.redhorizon.engine.scenegraph.scripting.Script +import nz.net.ultraq.redhorizon.filetypes.ImagesFile import org.joml.Vector2f import org.joml.primitives.Rectanglef @@ -41,6 +42,7 @@ import static org.lwjgl.glfw.GLFW.* import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.Executors +import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.ScheduledFuture import java.util.concurrent.TimeUnit @@ -54,7 +56,6 @@ class Player extends Node implements Rotatable, Temporal { private static final Logger logger = LoggerFactory.getLogger(Player) private static final Rectanglef MOVEMENT_RANGE = Map.MAX_BOUNDS private static final float MOVEMENT_SPEED = 200f - private static final float MOVEMENT_THRESHOLD = 0.2f private static final float ROTATION_SPEED = 180f private static final Vector2f up = new Vector2f(0, 1) @@ -66,7 +67,6 @@ class Player extends Node implements Rotatable, Temporal { private Vector2f velocity = new Vector2f() private Vector2f direction = new Vector2f() private Vector2f movement = new Vector2f() - private float movementHeading private Vector2f lookAt = new Vector2f() private Vector2f lastLookAt = new Vector2f() private Vector2f relativeLookAt = new Vector2f() @@ -99,6 +99,11 @@ class Player extends Node implements Rotatable, Temporal { velocity.set(0, 0) } + private final long rateOfFireMs = 1000 + private ScheduledFuture firingTask + private final ImagesFile bulletImagesFile + private ScheduledExecutorService firingService = Executors.newScheduledThreadPool(4) + /** * Constructor, load the sprite and scripts for the player. */ @@ -123,6 +128,8 @@ class Player extends Node implements Rotatable, Temporal { return true } + bulletImagesFile = resourceManager.loadFile('dragon.shp', ShpFile) + attachScript(new PlayerScript()) } @@ -159,7 +166,6 @@ class Player extends Node implements Rotatable, Temporal { Math.clamp(movement.x, MOVEMENT_RANGE.minX, MOVEMENT_RANGE.maxX), Math.clamp(movement.y, MOVEMENT_RANGE.minY, MOVEMENT_RANGE.maxY) ) - movementHeading = Math.toDegrees(velocity.angle(up)) as float } else { stop.execute() @@ -182,13 +188,21 @@ class Player extends Node implements Rotatable, Temporal { else if (direction.length()) { heading = Math.toDegrees(direction.angle(up)) as float } - else { - heading = movementHeading + else if (velocity.length()) { + heading = Math.toDegrees(velocity.angle(up)) as float } // Gamepad firing if (firing) { logger.debug('Firing') + firingTask = firingService.scheduleAtFixedRate({ -> + var bullet = new Bullet(bulletImagesFile, heading) + bullet.position = globalPosition + addChild(bullet) + }, 0, rateOfFireMs, TimeUnit.MILLISECONDS) + } + else { + firingTask?.cancel() } } @@ -252,41 +266,25 @@ class Player extends Node implements Rotatable, Temporal { // Gamepad controls scene.inputEventStream.addControls( - new GamepadControl(GLFW_GAMEPAD_AXIS_LEFT_X, 'Movement along the X axis', { value -> - if (value < -MOVEMENT_THRESHOLD || value > MOVEMENT_THRESHOLD) { - velocity.x = value - } - else { - velocity.x = 0 - } - }), - new GamepadControl(GLFW_GAMEPAD_AXIS_LEFT_Y, 'Movement along the Y axis', { value -> - if (value < -MOVEMENT_THRESHOLD || value > MOVEMENT_THRESHOLD) { - velocity.y = -value - } - else { - velocity.y = 0 - } - }), - new GamepadControl(GLFW_GAMEPAD_AXIS_RIGHT_X, 'Heading along the X axis', { value -> - if (value < -MOVEMENT_THRESHOLD || value > MOVEMENT_THRESHOLD) { - direction.x = value - } - else { - direction.x = 0 - } - }), - new GamepadControl(GLFW_GAMEPAD_AXIS_RIGHT_Y, 'Heading along the Y axis', { value -> - if (value < -MOVEMENT_THRESHOLD || value > MOVEMENT_THRESHOLD) { - direction.y = -value - } - else { - direction.y = 0 - } - }), - new GamepadControl(GLFW_GAMEPAD_AXIS_RIGHT_TRIGGER, 'Fire', { value -> - firing = value > -1f - }) + new GamepadControl(GLFW_GAMEPAD_AXIS_LEFT_X, 'Movement along the X axis', + { value -> velocity.x = value }, + { -> velocity.x = 0 } + ), + new GamepadControl(GLFW_GAMEPAD_AXIS_LEFT_Y, 'Movement along the Y axis', + { value -> velocity.y = -value }, + { -> velocity.y = 0 } + ), + new GamepadControl(GLFW_GAMEPAD_AXIS_RIGHT_X, 'Heading along the X axis', + { value -> direction.x = value }, + { -> direction.x = 0 } + ), + new GamepadControl(GLFW_GAMEPAD_AXIS_RIGHT_Y, 'Heading along the Y axis', + { value -> direction.y = -value }, + { -> direction.y = 0 } + ), + new GamepadControl(GLFW_GAMEPAD_AXIS_RIGHT_TRIGGER, 'Fire', + { value -> firing = value > -1f } + ) ) }