Any insight on how to make a twin stick shooter for the gamepad?
I’ve tried to backwards engineer some tutorials I saw done in Unity and other game engines but not sure how to get it to work perfectly.
I did something along these lines but there is some weird jittering and it’s not working perfectly. You can see in the Gif below. That stuttering is what I’m experiencing.
You are close! Here is a try on that script using deltas to rotate the entity, instead of calculating the final angle myself. It also includes a radial dead zone check, taken from an engine example script (link below).
Your corrected script:
var TwinShooter = pc.createScript('twinShooter');
TwinShooter.attributes.add('deadZoneLow', {
title: 'Low Dead Zone',
description: 'Radial thickness of inner dead zone of pad\'s joysticks. This dead zone ensures that all pads report a value of 0 for each joystick axis when untouched.',
type: 'number',
min: 0,
max: 0.4,
default: 0.2
});
TwinShooter.attributes.add('deadZoneHigh', {
title: 'High Dead Zone',
description: 'Radial thickness of outer dead zone of pad\'s joysticks. This dead zone ensures that all pads can reach the -1 and 1 limits of each joystick axis.',
type: 'number',
min: 0,
max: 0.4,
default: 0.2
});
TwinShooter.attributes.add('turnSpeed', {
title: 'Turn Speed',
description: 'Maximum turn speed in degrees per second',
type: 'number',
default: 150
});
// range from 0 to 1.
function applyRadialDeadZone(pos, remappedPos, deadZoneLow, deadZoneHigh) {
var magnitude = pos.length();
if (magnitude > deadZoneLow) {
var legalRange = 1 - deadZoneHigh - deadZoneLow;
var normalizedMag = Math.min(1, (magnitude - deadZoneLow) / legalRange);
var scale = normalizedMag / magnitude;
remappedPos.copy(pos).scale(scale);
} else {
remappedPos.set(0, 0);
}
}
TwinShooter.prototype.initialize = function () {
this.remappedPos = new pc.Vec2();
this.leftStick = {
center: new pc.Vec2(),
pos: new pc.Vec2()
};
this.rightStick = {
center: new pc.Vec2(),
pos: new pc.Vec2()
};
};
TwinShooter.prototype.update = function (dt) {
const gamepads = navigator.getGamepads ? navigator.getGamepads() : [];
for (var i = 0; i < gamepads.length; i++) {
const gamepad = gamepads[i];
// Only proceed if we have at least 2 sticks
if (gamepad && gamepad.mapping === 'standard' && gamepad.axes.length >= 4) {
let lookLeftRight = 0.0;
this.leftStick.pos.set(gamepad.axes[0], gamepad.axes[1]);
applyRadialDeadZone(this.leftStick.pos, this.remappedPos, this.deadZoneLow, this.deadZoneHigh);
lookLeftRight += -this.remappedPos.x * this.turnSpeed * dt;
this.rightStick.pos.set(gamepad.axes[2], gamepad.axes[3]);
applyRadialDeadZone(this.rightStick.pos, this.remappedPos, this.deadZoneLow, this.deadZoneHigh);
lookLeftRight += -this.remappedPos.x * this.turnSpeed * dt;
this.entity.rotate(0, lookLeftRight, 0);
}
}
};
This unfortunately doesn’t work as I was intending.
So when playing a twin stick shooter, you have one stick that will control basic movement and one that controls aiming direction. Depending on whether the game is 2d side scroller or 3rdperson overhead camera, both controls are highly Responsive to the player’s intention.
This simple rotation I actually am aware how to do, but it is not the movement type I’m looking for. I want it so that if I decided to point up to the right, I should already be there. Not waiting for it to rotate there. That type of rotation reminds me of the movement of a tank canon versus, a person with a shot gun doing a 180 to shoot a zombie behind them as they are running backwards at the same time. Do you feel me lol. I’ll send some examples for how this rotation should work. But the general gist is that it must be responsive and accurate.
Now some of these examples do use a keyboard and mouse, but I’m trying to utilize the gamepad. Also included other tutorials I saw. Either way thanks a ton for the help! Just looking for a different set up I suppose.
Ah makes sense, got it! You may be able to do that easily using a rotation vector or by calculating the rotation angle. Similar to what you were doing but without the issue you had.
Here is a quick try using vectors/lookAt @Robotpencil:
var TwinShooter = pc.createScript('twinShooter');
TwinShooter.attributes.add('deadZoneLow', {
title: 'Low Dead Zone',
description: 'Radial thickness of inner dead zone of pad\'s joysticks. This dead zone ensures that all pads report a value of 0 for each joystick axis when untouched.',
type: 'number',
min: 0,
max: 0.4,
default: 0.2
});
TwinShooter.attributes.add('deadZoneHigh', {
title: 'High Dead Zone',
description: 'Radial thickness of outer dead zone of pad\'s joysticks. This dead zone ensures that all pads can reach the -1 and 1 limits of each joystick axis.',
type: 'number',
min: 0,
max: 0.4,
default: 0.2
});
TwinShooter.attributes.add('moveSpeed', {
title: 'Move Speed',
description: 'Maximum move speed',
type: 'number',
default: 1
});
// range from 0 to 1.
function applyRadialDeadZone(pos, remappedPos, deadZoneLow, deadZoneHigh) {
var magnitude = pos.length();
if (magnitude > deadZoneLow) {
var legalRange = 1 - deadZoneHigh - deadZoneLow;
var normalizedMag = Math.min(1, (magnitude - deadZoneLow) / legalRange);
var scale = normalizedMag / magnitude;
remappedPos.copy(pos).scale(scale);
} else {
remappedPos.set(0, 0);
}
}
TwinShooter.prototype.initialize = function () {
this.vec = new pc.Vec3();
this.remappedPos = new pc.Vec2();
this.leftStick = {
center: new pc.Vec2(),
pos: new pc.Vec2()
};
this.rightStick = {
center: new pc.Vec2(),
pos: new pc.Vec2()
};
};
TwinShooter.prototype.update = function (dt) {
const gamepads = navigator.getGamepads ? navigator.getGamepads() : [];
for (var i = 0; i < gamepads.length; i++) {
const gamepad = gamepads[i];
// Only proceed if we have at least 2 sticks
if (gamepad && gamepad.mapping === 'standard' && gamepad.axes.length >= 4) {
this.leftStick.pos.set(gamepad.axes[0], gamepad.axes[1]);
applyRadialDeadZone(this.leftStick.pos, this.remappedPos, this.deadZoneLow, this.deadZoneHigh);
const moveX = -this.remappedPos.x * this.moveSpeed * dt;
const moveY = -this.remappedPos.y * this.moveSpeed * dt;
const moveDir = new pc.Vec3(-moveX, 0, -moveY);
this.entity.translate(moveDir);
this.rightStick.pos.set(gamepad.axes[2], gamepad.axes[3]);
applyRadialDeadZone(this.rightStick.pos, this.remappedPos, this.deadZoneLow, this.deadZoneHigh);
const lookX = -this.remappedPos.x;
const lookY = -this.remappedPos.y;
const lookDir = new pc.Vec3(lookX, 0, lookY);
// --- sensitivity
if (lookDir.length() > 0.5) {
const currentPos = this.entity.getPosition();
const lookAtPos = this.vec.copy(currentPos).add(lookDir);
this.entity.lookAt(lookAtPos);
}
}
}
};
There is a radial dead zone method, taken from the engine example scripts. You can tweak the parameters or fully remove it. And play with sensitivity threshold, you will find the comment.
For that this solution won’t work, since it uses a direction vector to look at the input direction.
You will have to grab the final angle, round it the closest step angle and reapply it.