Add touch joystick movement

This commit is contained in:
schmelczerandras 2020-10-23 18:06:56 +02:00
parent 23fa7646d6
commit 8b49b44ebf
7 changed files with 161 additions and 95 deletions

View file

@ -30,6 +30,7 @@
"eslint-plugin-unused-imports": "^0.1.3", "eslint-plugin-unused-imports": "^0.1.3",
"firebase": "^7.22.0", "firebase": "^7.22.0",
"gl-matrix": "^3.3.0", "gl-matrix": "^3.3.0",
"html-webpack-inline-source-plugin": "^1.0.0-beta.2",
"html-webpack-inline-svg-plugin": "^2.3.0", "html-webpack-inline-svg-plugin": "^2.3.0",
"html-webpack-plugin": "^4.5.0", "html-webpack-plugin": "^4.5.0",
"image-config-webpack-plugin": "^2.0.0", "image-config-webpack-plugin": "^2.0.0",
@ -41,7 +42,6 @@
"shared": "file:../shared", "shared": "file:../shared",
"socket.io-client": "^2.3.1", "socket.io-client": "^2.3.1",
"source-map-loader": "^1.1.1", "source-map-loader": "^1.1.1",
"style-ext-html-webpack-plugin": "^4.1.2",
"svg-url-loader": "^6.0.0", "svg-url-loader": "^6.0.0",
"terser-webpack-plugin": "^4.2.2", "terser-webpack-plugin": "^4.2.2",
"ts-config-webpack-plugin": "^2.0.0", "ts-config-webpack-plugin": "^2.0.0",

View file

@ -83,18 +83,25 @@ body {
width: 100%; width: 100%;
position: absolute; position: absolute;
top: 0; top: 0;
left: 0;
pointer-events: none; pointer-events: none;
user-select: none; user-select: none;
overflow: hidden; overflow: hidden;
& > * {
display: inline-block;
position: absolute;
top: 0;
left: 0;
}
h2 { h2 {
margin: $large-padding 0; margin: $large-padding 0;
} }
.player-tag { .player-tag {
position: absolute;
transform: translateX(-50%) translateY(-50%) rotate(-15deg); transform: translateX(-50%) translateY(-50%) rotate(-15deg);
transition: left 200ms, top 200ms; transition: left 200ms, top 200ms;
border-radius: 1000px; border-radius: 1000px;
@ -139,7 +146,6 @@ body {
} }
.ownership { .ownership {
position: absolute;
font-size: 0; font-size: 0;
transform: translateX(-50%) translateY(-50%); transform: translateX(-50%) translateY(-50%);
@ -165,6 +171,25 @@ body {
} }
} }
.joystick {
@include square($large-icon * 1.3);
background-color: white;
box-shadow: inset 0 0 8px 3px rgba(0, 0, 0, 0.33);
opacity: 0.35;
border-radius: 1000px;
div {
position: absolute;
top: 50%;
left: 50%;
background-color: #444;
box-shadow: 0 0 8px 3px rgba(0, 0, 0, 0.33);
@include square($small-icon);
border-radius: 1000px;
}
}
.planet-progress { .planet-progress {
position: absolute; position: absolute;
top: $small-padding; top: $small-padding;

View file

@ -0,0 +1,108 @@
import { vec2 } from 'gl-matrix';
import {
CommandGenerator,
SecondaryActionCommand,
TernaryActionCommand,
MoveActionCommand,
last,
} from 'shared';
import { Game } from '../../game';
import { OptionsHandler } from '../../options-handler';
export class TouchJoystickListener extends CommandGenerator {
private joystick: HTMLElement;
private joystickButton: HTMLElement;
private isJoystickActive = false;
private touchStartPosition!: vec2;
constructor(
target: HTMLElement,
private overlay: HTMLElement,
private readonly game: Game,
) {
super();
this.joystick = document.createElement('div');
this.joystick.className = 'joystick';
this.joystickButton = document.createElement('div');
this.joystick.appendChild(this.joystickButton);
target.addEventListener('touchstart', (event: TouchEvent) => {
event.preventDefault();
if (this.isJoystickActive) {
const center = vec2.fromValues(
last(event.touches)!.clientX,
last(event.touches)!.clientY,
);
this.sendCommandToSubcribers(
new SecondaryActionCommand(this.game.displayToWorldCoordinates(center)),
);
}
});
target.addEventListener('touchmove', (event: TouchEvent) => {
event.preventDefault();
if (!this.isJoystickActive) {
this.isJoystickActive = true;
this.overlay.appendChild(this.joystick);
this.touchStartPosition = vec2.fromValues(
event.touches[0].clientX,
event.touches[0].clientY,
);
this.joystickButton.style.transform = `translateX(-50%) translateY(-50%)`;
this.joystick.style.transform = `translateX(${this.touchStartPosition.x}px) translateY(${this.touchStartPosition.y}px) translateX(-50%) translateY(-50%)`;
}
const touchPosition = vec2.fromValues(
event.touches[0].clientX,
event.touches[0].clientY,
);
const movement = vec2.subtract(
vec2.create(),
touchPosition,
this.touchStartPosition,
);
vec2.scale(movement, movement, 1 / 3);
const length = vec2.length(movement);
const maxLength = 20;
vec2.scale(movement, movement, Math.min(1, maxLength / length));
this.joystickButton.style.transform = `translateX(${movement.x}px) translateY(${movement.y}px) translateX(-50%) translateY(-50%)`;
vec2.set(movement, movement.x, -movement.y);
if (vec2.squaredLength(movement) > 0) {
vec2.normalize(movement, movement);
this.sendCommandToSubcribers(
new MoveActionCommand(movement, OptionsHandler.options.relativeMovementEnabled),
);
}
});
target.addEventListener('touchend', (event: TouchEvent) => {
event.preventDefault();
if (!this.isJoystickActive) {
const center = vec2.fromValues(
event.changedTouches[0].clientX,
event.changedTouches[0].clientY,
);
this.sendCommandToSubcribers(
new SecondaryActionCommand(this.game.displayToWorldCoordinates(center)),
);
} else if (event.touches.length === 0) {
this.isJoystickActive = false;
this.joystick.parentElement?.removeChild(this.joystick);
this.sendCommandToSubcribers(
new MoveActionCommand(
vec2.create(),
OptionsHandler.options.relativeMovementEnabled,
),
);
}
});
}
}

View file

@ -1,80 +0,0 @@
import { vec2 } from 'gl-matrix';
import {
CommandGenerator,
PrimaryActionCommand,
SecondaryActionCommand,
TernaryActionCommand,
MoveActionCommand,
} from 'shared';
import { Game } from '../../game';
import { OptionsHandler } from '../../options-handler';
export class TouchListener extends CommandGenerator {
private previousPosition = vec2.create();
constructor(target: HTMLElement, private readonly game: Game) {
super();
target.addEventListener('touchstart', (event: TouchEvent) => {
event.preventDefault();
const touchCount = event.touches.length;
const position = this.positionFromEvent(event);
this.previousPosition = position;
if (touchCount == 1) {
this.sendCommandToSubcribers(new PrimaryActionCommand(position));
} else if (touchCount == 2) {
this.sendCommandToSubcribers(new SecondaryActionCommand(position));
} else {
this.sendCommandToSubcribers(new TernaryActionCommand(position));
}
});
target.addEventListener('touchmove', (event: TouchEvent) => {
event.preventDefault();
const position = this.positionFromEvent(event, true);
const movement = vec2.subtract(vec2.create(), position, this.previousPosition);
if (vec2.squaredLength(movement) > 0) {
vec2.normalize(movement, movement);
this.sendCommandToSubcribers(
new MoveActionCommand(movement, OptionsHandler.options.relativeMovementEnabled),
);
}
this.previousPosition = position;
});
target.addEventListener('touchend', (event: TouchEvent) => {
event.preventDefault();
this.sendCommandToSubcribers(
new MoveActionCommand(
vec2.create(),
OptionsHandler.options.relativeMovementEnabled,
),
);
});
}
private positionFromEvent(event: TouchEvent, invert = false): vec2 {
const touches = Array.prototype.slice.call(event.touches);
const center = touches.reduce(
(center: vec2, touch: Touch) =>
vec2.add(
center,
center,
vec2.fromValues(
touch.clientX * (invert ? -1 : 1),
touch.clientY * (invert ? -1 : 1),
),
),
vec2.create(),
);
return this.game.displayToWorldCoordinates(
vec2.scale(center, center, 1 / event.touches.length),
);
}
}

View file

@ -24,7 +24,7 @@ import {
import io from 'socket.io-client'; import io from 'socket.io-client';
import { KeyboardListener } from './commands/generators/keyboard-listener'; import { KeyboardListener } from './commands/generators/keyboard-listener';
import { MouseListener } from './commands/generators/mouse-listener'; import { MouseListener } from './commands/generators/mouse-listener';
import { TouchListener } from './commands/generators/touch-listener'; import { TouchJoystickListener } from './commands/generators/touch-joystick-listener';
import { CommandReceiverSocket } from './commands/receivers/command-receiver-socket'; import { CommandReceiverSocket } from './commands/receivers/command-receiver-socket';
import { PlayerDecision } from './join-form-handler'; import { PlayerDecision } from './join-form-handler';
import { GameObjectContainer } from './objects/game-object-container'; import { GameObjectContainer } from './objects/game-object-container';
@ -129,15 +129,15 @@ export class Game {
const delta = vec2.fromValues(deltaX, deltaY); const delta = vec2.fromValues(deltaX, deltaY);
const center = vec2.fromValues(width / 2, height / 2); const center = vec2.fromValues(width / 2, height / 2);
const p = vec2.add(center, center, delta); const p = vec2.add(center, center, delta);
const arrowPadding = 16; const arrowPadding = 24;
vec2.set( vec2.set(
p, p,
clamp(p.x, arrowPadding, width - 3 * arrowPadding), clamp(p.x, arrowPadding, width - arrowPadding),
clamp(height - p.y, arrowPadding, height - 3 * arrowPadding), clamp(height - p.y, arrowPadding, height - arrowPadding),
); );
e.style.transform = `translateX(${p.x}px) translateY(${p.y}px) rotate(${ e.style.transform = `translateX(${p.x}px) translateY(${
-angle + Math.PI / 2 p.y
}rad) translateX(-50%) translateY(-50%)`; }px) translateX(-50%) translateY(-50%) rotate(${-angle + Math.PI / 2}rad) `;
}); });
} else this.gameObjects.sendCommand(command); } else this.gameObjects.sendCommand(command);
}); });
@ -154,7 +154,7 @@ export class Game {
[ [
new KeyboardListener(document.body), new KeyboardListener(document.body),
new MouseListener(this.canvas, this), new MouseListener(this.canvas, this),
new TouchListener(this.canvas, this), new TouchJoystickListener(this.canvas, this.overlay, this),
], ],
[this.gameObjects, new CommandReceiverSocket(this.socket)], [this.gameObjects, new CommandReceiverSocket(this.socket)],
); );

View file

@ -4,8 +4,8 @@ const ScssConfigWebpackPlugin = require('scss-config-webpack-plugin');
const TsConfigWebpackPlugin = require('ts-config-webpack-plugin'); const TsConfigWebpackPlugin = require('ts-config-webpack-plugin');
const HtmlWebpackInlineSVGPlugin = require('html-webpack-inline-svg-plugin'); const HtmlWebpackInlineSVGPlugin = require('html-webpack-inline-svg-plugin');
const TerserJSPlugin = require('terser-webpack-plugin'); const TerserJSPlugin = require('terser-webpack-plugin');
const HtmlWebpackInlineSourcePlugin = require('html-webpack-inline-source-plugin');
//const FaviconsWebpackPlugin = require('favicons-webpack-plugin'); //const FaviconsWebpackPlugin = require('favicons-webpack-plugin');
//const StyleExtHtmlWebpackPlugin = require('style-ext-html-webpack-plugin');
module.exports = { module.exports = {
devServer: { devServer: {
@ -18,10 +18,14 @@ module.exports = {
plugins: [ plugins: [
// Cleans the dist folder before the build starts // Cleans the dist folder before the build starts
new CleanWebpackPlugin(), new CleanWebpackPlugin(),
new ScssConfigWebpackPlugin(),
// Generate a base html file and injects all generated css and js files // Generate a base html file and injects all generated css and js files
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
template: './src/index.html', template: './src/index.html',
inlineSource: '.(css)$',
}), }),
new HtmlWebpackInlineSourcePlugin(HtmlWebpackPlugin),
new HtmlWebpackInlineSVGPlugin({ new HtmlWebpackInlineSVGPlugin({
inlineAll: true, inlineAll: true,
svgoConfig: [ svgoConfig: [
@ -30,11 +34,10 @@ module.exports = {
}, },
], ],
}), }),
//new StyleExtHtmlWebpackPlugin('main.css'),
//new FaviconsWebpackPlugin('static/logo.svg'), //new FaviconsWebpackPlugin('static/logo.svg'),
// SCSS Configuration for .css .module.css and .scss .module.scss files // SCSS Configuration for .css .module.css and .scss .module.scss files
// see https://github.com/namics/webpack-config-plugins/tree/master/packages/scss-config-webpack-plugin/config // see https://github.com/namics/webpack-config-plugins/tree/master/packages/scss-config-webpack-plugin/config
new ScssConfigWebpackPlugin(),
// Multi threading typescript loader configuration with caching for .ts and .tsx files // Multi threading typescript loader configuration with caching for .ts and .tsx files
// see https://github.com/namics/webpack-config-plugins/tree/master/packages/ts-config-webpack-plugin/config // see https://github.com/namics/webpack-config-plugins/tree/master/packages/ts-config-webpack-plugin/config
new TsConfigWebpackPlugin(), new TsConfigWebpackPlugin(),
@ -57,6 +60,16 @@ module.exports = {
enforce: 'pre', enforce: 'pre',
use: ['source-map-loader'], use: ['source-map-loader'],
}, },
{
test: /\.css$/,
use: [
{
loader: 'style-loader',
options: { injectType: 'singletonStyleTag' },
},
'css-loader',
],
},
{ {
test: /\.svg/, test: /\.svg/,
use: { use: {

View file

@ -1,3 +1,3 @@
export function last<T>(a: Array<T>): T | null { export function last<T>(a: ArrayLike<T>): T | null {
return a.length > 0 ? a[a.length - 1] : null; return a.length > 0 ? a[a.length - 1] : null;
} }