Skip to content

Commit

Permalink
Merge pull request #8 from itsdorosh/gamepad-support
Browse files Browse the repository at this point in the history
add gamepad support
  • Loading branch information
itsdorosh authored Oct 1, 2020
2 parents 243fd90 + e77f39f commit c91cfb5
Show file tree
Hide file tree
Showing 8 changed files with 211 additions and 41 deletions.
72 changes: 60 additions & 12 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,19 @@ <h1 id="gameOverCaption" class="hide">GAME OVER</h1>
<button id="playButton">Play ▶️</button>
<button id="restartButton" class="hide">Restart 🔄</button>
</div>

<div id="generalButtons" class="position-absolute">
<span id="fullscreenMode" onclick="toggleFullScreen()">📺️</span>
<span id="gamepad" class="hide">🎮</span>
<span id="mouse" class="hide">🖱</span>
</div>

<div id="canvasContainer"></div>

<script src="https://unpkg.com/[email protected]/build/three.js"></script>
<script src="./src/constants.js"></script>
<script src="src/enemies.js"></script>
<script src="./src/helpers.js"></script>
<script src="./src/controls.js"></script>
<script src="./src/raycaster.js"></script>
<script src="./src/viewer.js"></script>
Expand All @@ -37,6 +45,8 @@ <h1 id="gameOverCaption" class="hide">GAME OVER</h1>
const playButton = document.getElementById('playButton');
const pauseButton = document.getElementById('pauseButton');
const restartButton = document.getElementById('restartButton');
const gamepadButton = document.getElementById('gamepad');
const mouseButton = document.getElementById('mouse');

// captions
const greetingCaption = document.getElementById('greetingCaption');
Expand All @@ -52,31 +62,69 @@ <h1 id="gameOverCaption" class="hide">GAME OVER</h1>
});

engine.onGameOver(() => {
gameButtons.classList.remove('hide');
greetingCaption.classList.add('hide');
gameOverCaption.classList.remove('hide');
playButton.classList.add('hide');
restartButton.classList.remove('hide');
hide(greetingCaption, playButton);
show(gameButtons, gameOverCaption, restartButton);
});

playButton.addEventListener('click', () => {
gameButtons.classList.add('hide');
inGameButtons.classList.remove('hide');
hide(gameButtons);
show(inGameButtons);
engine.play();
});

pauseButton.addEventListener('click', () => {
gameButtons.classList.remove('hide');
restartButton.classList.remove('hide');
show(gameButtons, restartButton, playButton);
engine.pause();
});

restartButton.addEventListener('click', () => {
greetingCaption.classList.add('hide');
gameOverCaption.classList.remove('hide');
gameButtons.classList.add('hide');
hide(gameOverCaption, gameButtons);
show(greetingCaption);
engine.restart();
});

gamepadButton.addEventListener('click', () => controls.switchControlsMode(CONTROLS_MODES.GAMEPAD_MODE));

mouseButton.addEventListener('click', () => controls.switchControlsMode(CONTROLS_MODES.MOUSE_MODE));

controls.on('gamepadconnected', () => {
controls.switchControlsMode(CONTROLS_MODES.MOUSE_MODE);
});

controls.on('gamepaddisconnected', () => {
controls.switchControlsMode(CONTROLS_MODES.NO_MODE);
});

controls.on('controlsModeSwitched', (mode) => {
switch (mode) {
case CONTROLS_MODES.MOUSE_MODE: {
hide(mouseButton);
show(gamepadButton);
break;
}

case CONTROLS_MODES.GAMEPAD_MODE: {
hide(gamepadButton);
show(mouseButton);
break;
}

case CONTROLS_MODES.NO_MODE: {
hide(mouseButton, gamepadButton);
break;
}
}
});

function toggleFullScreen() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen();
} else {
if (document.exitFullscreen) {
document.exitFullscreen();
}
}
}
</script>
</body>
</html>
19 changes: 19 additions & 0 deletions main.css
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ body {
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* Internet Explorer/Edge */
user-select: none;
overflow: hidden;
}

button {
Expand Down Expand Up @@ -56,6 +57,24 @@ button {
background: rgba(102, 51, 153, 0.6);
}

#generalButtons {
display: flex;
right: 10px;
bottom: 10px;
font-size: 24px;
}

#generalButtons span {
margin-left: 15px;
}

.aim {
font-size: 72px;
margin: 0;
padding: 0;
line-height: 0;
}

.position-absolute {
position: absolute;
}
Expand Down
8 changes: 8 additions & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,11 @@ const DEADLINE = 10;
const RANGE_X = 10;

const RANGE_Y = 2;

const CONTROLS_MODES = {
NO_MODE: 'NO_MODE',
MOUSE_MODE: 'MOUSE_MODE',
GAMEPAD_MODE: 'GAMEPAD_MODE',
VR_MODE: 'VR_MODE', // for future
AR_MODE: 'AR_MODE', // for future
}
76 changes: 72 additions & 4 deletions src/controls.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,93 @@
* you can simply subscribe on some of AVAILABLE_DOM_EVENTS
*/
class Controls {
rootObject;

constructor(rootObject = document.body) {
this.eventStorage = {};
this.rootObject = rootObject;
this.currentControlsMode = CONTROLS_MODES.MOUSE_MODE;

this.aim = document.createElement('p');
this.aim.innerText = '+';
this.aim.classList.add('aim', 'position-absolute');
hide(this.aim);
this.rootObject.appendChild(this.aim);

this.init();
}

/**
* @method init
*/
init() {
window.addEventListener("gamepadconnected", ({ gamepad }) => {
this.dispatch('gamepadconnected', gamepad);
});

window.addEventListener("gamepaddisconnected", ({ gamepad }) => {
this.dispatch('gamepaddisconnected', gamepad);
});

AVAILABLE_DOM_EVENTS.forEach((eventName) => {
this.rootObject.addEventListener(eventName, (event) => {
this.dispatch(eventName, event);
});
});
}

switchControlsMode(mode) {
if (mode in CONTROLS_MODES) {
this.currentControlsMode = mode;
this.handleSwitchControlsMode(mode);
this.dispatch('controlsModeSwitched', mode);
}
}

handleSwitchControlsMode(mode) {
if(mode === CONTROLS_MODES.GAMEPAD_MODE) {
show(this.aim);
this.aim.style.marginLeft = `-${39.59 / 2}px`;
this.moveAimToCoordinates(0, 0);
}

else if(mode === CONTROLS_MODES.MOUSE_MODE || mode === CONTROLS_MODES.NO_MODE) {
hide(this.aim);
}
}

moveAimToCoordinates(x, y) {
const pos = convertCoordinatesToPixels(
this.rootObject.offsetWidth,
this.rootObject.offsetHeight,
x, y
);

this.aim.style.left = `${pos.x}px`;
this.aim.style.top = `${pos.y}px`;
}

handleGamepadButtons() {
if (this.currentControlsMode === CONTROLS_MODES.GAMEPAD_MODE) {

const gamepads = navigator.getGamepads ? navigator.getGamepads() : [];
if (!gamepads) {
return;
}

const gp = gamepads[0];
if (gp.buttons[0].pressed) {
this.dispatch('shot1', convertCoordinatesToPixels(
this.rootObject.offsetWidth,
this.rootObject.offsetHeight,
gp.axes[0].toFixed(1),
gp.axes[1].toFixed(1)
));
}

this.moveAimToCoordinates(gp.axes[0].toFixed(1), gp.axes[1].toFixed(1))
}
}

/**
* @method on
* @description this method should assign some callback to eventName and prevent secondary assignment
Expand All @@ -41,11 +109,11 @@ class Controls {
* @method dispatch
* @description this method should provide way for trigger some callback by eventName
* @param eventName
* @param DOMEvent
* @param data
*/
dispatch(eventName, DOMEvent) {
dispatch(eventName, data) {
if (eventName in this.eventStorage) {
this.eventStorage[eventName](DOMEvent);
this.eventStorage[eventName](data);
} else {
throw(`there is no callback for ${eventName} event name`);
}
Expand Down
21 changes: 16 additions & 5 deletions src/engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ class Engine {
_generateEnemiesIntervalId;
_onGameOverCallback;
gameEventCallbackMap;
__defaultAnimationAction = (enemyMeshes) => this.updateExistingEnemiesPosition(enemyMeshes);
__updateEnemiesDefaultAnimationAction = (enemyMeshes) => this.updateExistingEnemiesPosition(enemyMeshes);
__handleGamepadButtonsDefaultAnimationAction = () => this.controls.handleGamepadButtons();

/**
* @constructor throw dependencies here
Expand All @@ -19,8 +20,10 @@ class Engine {
this.controls = controls;
this.raycaster = raycaster;

this.viewer.addAnimationActions(this.__handleGamepadButtonsDefaultAnimationAction);

this.gameEventCallbackMap = {
'shot': ({ clientX, clientY }) => this.onShot({clientX, clientY}),
'shot': ({ clientX, clientY }) => this.onShot({x: clientX, y: clientY}),
};

this.init();
Expand All @@ -35,6 +38,10 @@ class Engine {
});
}

this.controls.on('shot1', (pos) => {
this.onShot(pos);
});

this.raycaster.onIntersection((UUID) => {
this.onHit(UUID);
});
Expand Down Expand Up @@ -63,7 +70,11 @@ class Engine {
}

play() {
this.viewer.setAnimationAction(this.__defaultAnimationAction);
this.viewer.addAnimationActions(
this.__updateEnemiesDefaultAnimationAction,
this.__handleGamepadButtonsDefaultAnimationAction
);

this._generateEnemiesIntervalId = setInterval(
() => this.generateEnemies(),
GENERATE_ENEMIES_INTERVAL_TIME
Expand All @@ -72,7 +83,7 @@ class Engine {

pause() {
clearInterval(this._generateEnemiesIntervalId);
this.viewer.removeAnimationAction();
this.viewer.clearAnimationActions();
}

onShot(coordinates) {
Expand All @@ -89,7 +100,7 @@ class Engine {

gameOver() {
clearInterval(this._generateEnemiesIntervalId);
this.viewer.removeAnimationAction();
this.viewer.clearAnimationActions();
this._onGameOverCallback();
}

Expand Down
24 changes: 24 additions & 0 deletions src/helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
function getRandomInt(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1) + min);
}

function getRandomFloat(min, max) {
return Math.random() * (max - min) + min;
}

function hide(...elements) {
elements.forEach(element => element.classList.add('hide'));
}

function show(...elements) {
elements.forEach(element => element.classList.remove('hide'));
}

function convertCoordinatesToPixels(width, height, x = 0, y = 0) {
return {
x: ((width / 2) + ((width / 2) * x)),
y: ((height / 2) + ((height / 2) * y)),
};
}
4 changes: 2 additions & 2 deletions src/raycaster.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ class Raycaster {
* @param intersectionParams - object with three fields: coordinates, container & types
*/
intersect(intersectionParams) {
this.mouse.x = (intersectionParams.coordinates.clientX / window.innerWidth) * 2 - 1;
this.mouse.y = -(intersectionParams.coordinates.clientY / window.innerHeight) * 2 + 1;
this.mouse.x = (intersectionParams.coordinates.x / window.innerWidth) * 2 - 1;
this.mouse.y = -(intersectionParams.coordinates.y / window.innerHeight) * 2 + 1;

this.raycaster.setFromCamera(this.mouse, this.camera);
const intersects = this.raycaster.intersectObjects(intersectionParams.container.children);
Expand Down
Loading

0 comments on commit c91cfb5

Please sign in to comment.