Игровая логика PvP: сервер (C++) + клиент (Quintus.js)
Сервер: Game — authoritative-тик 20 Гц через uSockets timer, движение/коллизии танков и пуль, разрушаемые кирпичные стены, respawn, победа/поражение/ничья, технический проигрыш при дисконнекте. game.ready запускает матч, когда оба игрока в pvp-комнате готовы. Клиент: заведён Quintus.js (MIT, vendored) — Tank/Bullet/Wall спрайты, рендер по снапшотам game.state, keyboard-инпут (WASD/стрелки/space), экран результата матча. Найден и исправлен use-after-free: колбэк onFinished_ синхронно удалял Game изнутри его же метода — перенесено на uWS::Loop::defer.
This commit is contained in:
+986
@@ -0,0 +1,986 @@
|
||||
/*global Quintus:false, module:false */
|
||||
|
||||
/**
|
||||
Quintus HTML5 Game Engine - Input Module
|
||||
|
||||
The code in `quintus_input.js` defines the `Quintus.Input` module, which
|
||||
concerns itself with game-type (pretty anything besides touchscreen input)
|
||||
|
||||
@module Quintus.Input
|
||||
*/
|
||||
|
||||
|
||||
var quintusInput = function(Quintus) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Quintus Input Module
|
||||
*
|
||||
* @class Quintus.Input
|
||||
*/
|
||||
Quintus.Input = function(Q) {
|
||||
/**
|
||||
* Provided key names mapped to key codes - add more names and key codes as necessary
|
||||
*
|
||||
* @for Quintus.Input
|
||||
* @property KEY_NAMES
|
||||
* @type Object
|
||||
* @static
|
||||
*/
|
||||
var KEY_NAMES = Q.KEY_NAMES = {
|
||||
LEFT: 37, RIGHT: 39,
|
||||
UP: 38, DOWN: 40,
|
||||
|
||||
ZERO : 48, ONE : 49, TWO : 50,
|
||||
THREE : 51, FOUR : 52, FIVE : 53,
|
||||
SIX : 54, SEVEN : 55, EIGHT : 56,
|
||||
NINE : 57,
|
||||
|
||||
A : 65, B : 66, C : 67,
|
||||
D : 68, E : 69, F : 70,
|
||||
G : 71, H : 72, I : 73,
|
||||
J : 74, K : 75, L : 76,
|
||||
M : 77, N : 78, O : 79,
|
||||
P : 80, Q : 81, R : 82,
|
||||
S : 83, T : 84, U : 85,
|
||||
V : 86, W : 87, X : 88,
|
||||
Y : 89, Z : 90,
|
||||
|
||||
ENTER: 13,
|
||||
ESC: 27,
|
||||
BACKSPACE : 8,
|
||||
TAB : 9,
|
||||
SHIFT : 16,
|
||||
CTRL : 17,
|
||||
ALT : 18,
|
||||
SPACE: 32,
|
||||
|
||||
HOME : 36, END : 35,
|
||||
PGGUP : 33, PGDOWN : 34
|
||||
};
|
||||
|
||||
var DEFAULT_KEYS = {
|
||||
LEFT: 'left', RIGHT: 'right',
|
||||
UP: 'up', DOWN: 'down',
|
||||
SPACE: 'fire',
|
||||
Z: 'fire',
|
||||
X: 'action',
|
||||
ENTER: 'confirm',
|
||||
ESC: 'esc',
|
||||
P: 'P',
|
||||
S: 'S'
|
||||
};
|
||||
|
||||
var DEFAULT_TOUCH_CONTROLS = [ ['left','<' ],
|
||||
['right','>' ],
|
||||
[],
|
||||
['action','b'],
|
||||
['fire', 'a' ]];
|
||||
|
||||
// Clockwise from midnight (a la CSS)
|
||||
var DEFAULT_JOYPAD_INPUTS = [ 'up','right','down','left'];
|
||||
|
||||
/**
|
||||
* Current state of bound inputs
|
||||
*
|
||||
* @for Quintus.Input
|
||||
* @property Q.inputs
|
||||
* @type Object
|
||||
*/
|
||||
Q.inputs = {};
|
||||
Q.joypad = {};
|
||||
|
||||
var hasTouch = !!('ontouchstart' in window);
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* Convert a canvas point to a stage point, x dimension
|
||||
*
|
||||
* @method Q.canvasToStageX
|
||||
* @for Quintus.Input
|
||||
* @param {Float} x
|
||||
* @param {Q.Stage} stage
|
||||
* @returns {Integer} x
|
||||
*/
|
||||
Q.canvasToStageX = function(x,stage) {
|
||||
x = x / Q.cssWidth * Q.width;
|
||||
if(stage.viewport) {
|
||||
x /= stage.viewport.scale;
|
||||
x += stage.viewport.x;
|
||||
}
|
||||
|
||||
return x;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* Convert a canvas point to a stage point, y dimension
|
||||
*
|
||||
* @method Q.canvasToStageY
|
||||
* @param {Float} y
|
||||
* @param {Q.Stage} stage
|
||||
* @returns {Integer} y
|
||||
*/
|
||||
Q.canvasToStageY = function(y,stage) {
|
||||
y = y / Q.cssWidth * Q.width;
|
||||
if(stage.viewport) {
|
||||
y /= stage.viewport.scale;
|
||||
y += stage.viewport.y;
|
||||
}
|
||||
|
||||
return y;
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* Button and mouse input subsystem for Quintus.
|
||||
* An instance of this class is auto-created as {{#crossLink "Q.input"}}{{/crossLink}}
|
||||
*
|
||||
* @class Q.InputSystem
|
||||
* @extends Q.Evented
|
||||
* @for Quintus.Input
|
||||
*/
|
||||
Q.InputSystem = Q.Evented.extend({
|
||||
keys: {},
|
||||
keypad: {},
|
||||
keyboardEnabled: false,
|
||||
touchEnabled: false,
|
||||
joypadEnabled: false,
|
||||
|
||||
/**
|
||||
* Bind a key name or keycode to an action name (used by `keyboardControls`)
|
||||
*
|
||||
* @method bindKey
|
||||
* @for Q.InputSystem
|
||||
* @param {String or Integer} key - name or integer keycode for to bind
|
||||
* @param {String} name - name of action to bind to
|
||||
*/
|
||||
bindKey: function(key,name) {
|
||||
Q.input.keys[KEY_NAMES[key] || key] = name;
|
||||
},
|
||||
|
||||
/**
|
||||
* Enable keyboard controls by binding to events
|
||||
*
|
||||
* @for Q.InputSystem
|
||||
* @method enableKeyboard
|
||||
*/
|
||||
enableKeyboard: function() {
|
||||
if(this.keyboardEnabled) { return false; }
|
||||
|
||||
// Make selectable and remove an :focus outline
|
||||
Q.el.tabIndex = 0;
|
||||
Q.el.style.outline = 0;
|
||||
|
||||
Q.el.addEventListener("keydown",function(e) {
|
||||
if(Q.input.keys[e.keyCode]) {
|
||||
var actionName = Q.input.keys[e.keyCode];
|
||||
Q.inputs[actionName] = true;
|
||||
Q.input.trigger(actionName);
|
||||
Q.input.trigger('keydown',e.keyCode);
|
||||
}
|
||||
if(!e.ctrlKey && !e.metaKey) {
|
||||
e.preventDefault();
|
||||
}
|
||||
},false);
|
||||
|
||||
Q.el.addEventListener("keyup",function(e) {
|
||||
if(Q.input.keys[e.keyCode]) {
|
||||
var actionName = Q.input.keys[e.keyCode];
|
||||
Q.inputs[actionName] = false;
|
||||
Q.input.trigger(actionName + "Up");
|
||||
Q.input.trigger('keyup',e.keyCode);
|
||||
}
|
||||
e.preventDefault();
|
||||
},false);
|
||||
|
||||
if(Q.options.autoFocus) { Q.el.focus(); }
|
||||
this.keyboardEnabled = true;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Convenience method to activate keyboard controls (call `bindKey` and `enableKeyboard` internally)
|
||||
*
|
||||
* @method keyboardControls
|
||||
* @for Q.InputSystem
|
||||
* @param {Object} [keys] - hash of key names or codes to actions
|
||||
*/
|
||||
keyboardControls: function(keys) {
|
||||
keys = keys || DEFAULT_KEYS;
|
||||
Q._each(keys,function(name,key) {
|
||||
this.bindKey(key,name);
|
||||
},Q.input);
|
||||
this.enableKeyboard();
|
||||
},
|
||||
|
||||
_containerOffset: function() {
|
||||
Q.input.offsetX = 0;
|
||||
Q.input.offsetY = 0;
|
||||
var el = Q.el;
|
||||
do {
|
||||
Q.input.offsetX += el.offsetLeft;
|
||||
Q.input.offsetY += el.offsetTop;
|
||||
} while(el = el.offsetParent);
|
||||
},
|
||||
|
||||
touchLocation: function(touch) {
|
||||
var el = Q.el,
|
||||
posX = touch.offsetX,
|
||||
posY = touch.offsetY,
|
||||
touchX, touchY;
|
||||
|
||||
if(Q._isUndefined(posX) || Q._isUndefined(posY)) {
|
||||
posX = touch.layerX;
|
||||
posY = touch.layerY;
|
||||
}
|
||||
|
||||
if(Q._isUndefined(posX) || Q._isUndefined(posY)) {
|
||||
if(Q.input.offsetX === void 0) { Q.input._containerOffset(); }
|
||||
posX = touch.pageX - Q.input.offsetX;
|
||||
posY = touch.pageY - Q.input.offsetY;
|
||||
}
|
||||
|
||||
touchX = Q.width * posX / Q.cssWidth;
|
||||
touchY = Q.height * posY / Q.cssHeight;
|
||||
|
||||
|
||||
return { x: touchX, y: touchY };
|
||||
},
|
||||
|
||||
/**
|
||||
* Activate touch button controls - pass in an options hash to override
|
||||
*
|
||||
* Default Options:
|
||||
*
|
||||
* {
|
||||
* left: 0,
|
||||
* gutter:10,
|
||||
* controls: DEFAULT_TOUCH_CONTROLS,
|
||||
* width: Q.width,
|
||||
* bottom: Q.height
|
||||
* }
|
||||
*
|
||||
* Default controls are left and right buttons, a space, and 'a' and 'b' buttons, as defined as an Array of Arrays below:
|
||||
*
|
||||
* [ ['left','<' ],
|
||||
* ['right','>' ],
|
||||
* [], // use an empty array as a spacer
|
||||
* ['action','b'],
|
||||
* ['fire', 'a' ]]
|
||||
*
|
||||
* @method touchControls
|
||||
* @for Q.InputSystem
|
||||
* @param {Object} [opts] - Options hash
|
||||
*/
|
||||
touchControls: function(opts) {
|
||||
if(this.touchEnabled) { return false; }
|
||||
if(!hasTouch) { return false; }
|
||||
|
||||
Q.input.keypad = opts = Q._extend({
|
||||
left: 0,
|
||||
gutter:10,
|
||||
controls: DEFAULT_TOUCH_CONTROLS,
|
||||
width: Q.width,
|
||||
bottom: Q.height,
|
||||
fullHeight: false
|
||||
},opts);
|
||||
|
||||
opts.unit = (opts.width / opts.controls.length);
|
||||
opts.size = opts.unit - (opts.gutter * 2);
|
||||
|
||||
function getKey(touch) {
|
||||
var pos = Q.input.touchLocation(touch),
|
||||
minY = opts.bottom - opts.unit;
|
||||
for(var i=0,len=opts.controls.length;i<len;i++) {
|
||||
var minX = i * opts.unit + opts.gutter;
|
||||
if(pos.x >= minX && pos.x <= (minX+opts.size) && (opts.fullHeight || (pos.y >= minY + opts.gutter && pos.y <= (minY+opts.unit - opts.gutter))))
|
||||
{
|
||||
return opts.controls[i][0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function touchDispatch(event) {
|
||||
var wasOn = {},
|
||||
i, len, tch, key, actionName;
|
||||
|
||||
// Reset all the actions bound to controls
|
||||
// but keep track of all the actions that were on
|
||||
for(i=0,len = opts.controls.length;i<len;i++) {
|
||||
actionName = opts.controls[i][0];
|
||||
if(Q.inputs[actionName]) { wasOn[actionName] = true; }
|
||||
Q.inputs[actionName] = false;
|
||||
}
|
||||
|
||||
var touches = event.touches ? event.touches : [ event ];
|
||||
|
||||
for(i=0,len=touches.length;i<len;i++) {
|
||||
tch = touches[i];
|
||||
key = getKey(tch);
|
||||
|
||||
if(key) {
|
||||
// Mark this input as on
|
||||
Q.inputs[key] = true;
|
||||
|
||||
// Either trigger a new action
|
||||
// or remove from wasOn list
|
||||
if(!wasOn[key]) {
|
||||
Q.input.trigger(key);
|
||||
} else {
|
||||
delete wasOn[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Any remaining were on the last frame
|
||||
// and need to trigger an up action
|
||||
for(actionName in wasOn) {
|
||||
Q.input.trigger(actionName + "Up");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
this.touchDispatchHandler = function(e) {
|
||||
touchDispatch(e);
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
|
||||
Q._each(["touchstart","touchend","touchmove","touchcancel"],function(evt) {
|
||||
Q.el.addEventListener(evt,this.touchDispatchHandler);
|
||||
},this);
|
||||
|
||||
this.touchEnabled = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Turn off touch (button and joypad) controls and remove event listeners
|
||||
*
|
||||
* @method disableTouchControls
|
||||
* @for Q.InputSystem
|
||||
*/
|
||||
disableTouchControls: function() {
|
||||
Q._each(["touchstart","touchend","touchmove","touchcancel"],function(evt) {
|
||||
Q.el.removeEventListener(evt,this.touchDispatchHandler);
|
||||
},this);
|
||||
|
||||
Q.el.removeEventListener('touchstart',this.joypadStart);
|
||||
Q.el.removeEventListener('touchmove',this.joypadMove);
|
||||
Q.el.removeEventListener('touchend',this.joypadEnd);
|
||||
Q.el.removeEventListener('touchcancel',this.joypadEnd);
|
||||
this.touchEnabled = false;
|
||||
|
||||
// clear existing inputs
|
||||
for(var input in Q.inputs) {
|
||||
Q.inputs[input] = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Activate joypad controls (i.e. 4-way touch controls)
|
||||
*
|
||||
* Lots of options, defaults are:
|
||||
*
|
||||
* {
|
||||
* size: 50,
|
||||
* trigger: 20,
|
||||
* center: 25,
|
||||
* color: "#CCC",
|
||||
* background: "#000",
|
||||
* alpha: 0.5,
|
||||
* zone: Q.width / 2,
|
||||
* inputs: DEFAULT_JOYPAD_INPUTS
|
||||
* }
|
||||
*
|
||||
* Default joypad controls is an array that defines the inputs to bind to:
|
||||
*
|
||||
* // Clockwise from midnight (a la CSS)
|
||||
* var DEFAULT_JOYPAD_INPUTS = [ 'up','right','down','left'];
|
||||
*
|
||||
* @method joypadControls
|
||||
* @for Q.InputSystem
|
||||
* @param {Object} [opts] - joypad options
|
||||
*/
|
||||
joypadControls: function(opts) {
|
||||
if(this.joypadEnabled) { return false; }
|
||||
if(!hasTouch) { return false; }
|
||||
|
||||
var joypad = Q.joypad = Q._defaults(opts || {},{
|
||||
size: 50,
|
||||
trigger: 20,
|
||||
center: 25,
|
||||
color: "#CCC",
|
||||
background: "#000",
|
||||
alpha: 0.5,
|
||||
zone: Q.width / 2,
|
||||
joypadTouch: null,
|
||||
inputs: DEFAULT_JOYPAD_INPUTS,
|
||||
triggers: []
|
||||
});
|
||||
|
||||
this.joypadStart = function(evt) {
|
||||
if(joypad.joypadTouch === null) {
|
||||
var touch = evt.changedTouches[0],
|
||||
loc = Q.input.touchLocation(touch);
|
||||
|
||||
if(loc.x < joypad.zone) {
|
||||
joypad.joypadTouch = touch.identifier;
|
||||
joypad.centerX = loc.x;
|
||||
joypad.centerY = loc.y;
|
||||
joypad.x = null;
|
||||
joypad.y = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
this.joypadMove = function(e) {
|
||||
if(joypad.joypadTouch !== null) {
|
||||
var evt = e;
|
||||
|
||||
for(var i=0,len=evt.changedTouches.length;i<len;i++) {
|
||||
var touch = evt.changedTouches[i];
|
||||
|
||||
if(touch.identifier === joypad.joypadTouch) {
|
||||
var loc = Q.input.touchLocation(touch),
|
||||
dx = loc.x - joypad.centerX,
|
||||
dy = loc.y - joypad.centerY,
|
||||
dist = Math.sqrt(dx * dx + dy * dy),
|
||||
overage = Math.max(1,dist / joypad.size),
|
||||
ang = Math.atan2(dx,dy);
|
||||
|
||||
if(overage > 1) {
|
||||
dx /= overage;
|
||||
dy /= overage;
|
||||
dist /= overage;
|
||||
}
|
||||
|
||||
var triggers = [
|
||||
dy < -joypad.trigger,
|
||||
dx > joypad.trigger,
|
||||
dy > joypad.trigger,
|
||||
dx < -joypad.trigger
|
||||
];
|
||||
|
||||
for(var k=0;k<triggers.length;k++) {
|
||||
var actionName = joypad.inputs[k];
|
||||
if(triggers[k]) {
|
||||
Q.inputs[actionName] = true;
|
||||
|
||||
if(!joypad.triggers[k]) {
|
||||
Q.input.trigger(actionName);
|
||||
}
|
||||
} else {
|
||||
Q.inputs[actionName] = false;
|
||||
if(joypad.triggers[k]) {
|
||||
Q.input.trigger(actionName + "Up");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Q._extend(joypad, {
|
||||
dx: dx, dy: dy,
|
||||
x: joypad.centerX + dx,
|
||||
y: joypad.centerY + dy,
|
||||
dist: dist,
|
||||
ang: ang,
|
||||
triggers: triggers
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
this.joypadEnd = function(e) {
|
||||
var evt = e;
|
||||
|
||||
if(joypad.joypadTouch !== null) {
|
||||
for(var i=0,len=evt.changedTouches.length;i<len;i++) {
|
||||
var touch = evt.changedTouches[i];
|
||||
if(touch.identifier === joypad.joypadTouch) {
|
||||
for(var k=0;k<joypad.triggers.length;k++) {
|
||||
var actionName = joypad.inputs[k];
|
||||
Q.inputs[actionName] = false;
|
||||
if(joypad.triggers[k]) {
|
||||
Q.input.trigger(actionName + "Up");
|
||||
}
|
||||
}
|
||||
joypad.joypadTouch = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
Q.el.addEventListener("touchstart",this.joypadStart);
|
||||
Q.el.addEventListener("touchmove",this.joypadMove);
|
||||
Q.el.addEventListener("touchend",this.joypadEnd);
|
||||
Q.el.addEventListener("touchcancel",this.joypadEnd);
|
||||
|
||||
this.joypadEnabled = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Activate mouse controls - mouse controls don't trigger events, but just set `Q.inputs['mouseX']` & `Q.inputs['mouseY']` on each frame.
|
||||
*
|
||||
* Default options:
|
||||
*
|
||||
* {
|
||||
* stageNum: 0,
|
||||
* mouseX: "mouseX",
|
||||
* mouseY: "mouseY",
|
||||
* cursor: "off"
|
||||
* }
|
||||
*
|
||||
* @method mouseControls
|
||||
* @for Q.InputSystem
|
||||
* @param {Object} [options] - override default options
|
||||
*/
|
||||
mouseControls: function(options) {
|
||||
options = options || {};
|
||||
|
||||
var stageNum = options.stageNum || 0;
|
||||
var mouseInputX = options.mouseX || "mouseX";
|
||||
var mouseInputY = options.mouseY || "mouseY";
|
||||
var cursor = options.cursor || "off";
|
||||
|
||||
var mouseMoveObj = {};
|
||||
|
||||
if(cursor !== "on") {
|
||||
if(cursor === "off") {
|
||||
Q.el.style.cursor = 'none';
|
||||
}
|
||||
else {
|
||||
Q.el.style.cursor = cursor;
|
||||
}
|
||||
}
|
||||
|
||||
Q.inputs[mouseInputX] = 0;
|
||||
Q.inputs[mouseInputY] = 0;
|
||||
|
||||
Q._mouseMove = function(e) {
|
||||
e.preventDefault();
|
||||
var touch = e.touches ? e.touches[0] : e;
|
||||
var el = Q.el,
|
||||
rect = el.getBoundingClientRect(),
|
||||
style = window.getComputedStyle(el),
|
||||
posX = touch.clientX - rect.left - parseInt(style.paddingLeft, 10),
|
||||
posY = touch.clientY - rect.top - parseInt(style.paddingTop, 10);
|
||||
|
||||
var stage = Q.stage(stageNum);
|
||||
|
||||
if(Q._isUndefined(posX) || Q._isUndefined(posY)) {
|
||||
posX = touch.offsetX;
|
||||
posY = touch.offsetY;
|
||||
}
|
||||
|
||||
if(Q._isUndefined(posX) || Q._isUndefined(posY)) {
|
||||
posX = touch.layerX;
|
||||
posY = touch.layerY;
|
||||
}
|
||||
|
||||
if(Q._isUndefined(posX) || Q._isUndefined(posY)) {
|
||||
if(Q.input.offsetX === void 0) { Q.input._containerOffset(); }
|
||||
posX = touch.pageX - Q.input.offsetX;
|
||||
posY = touch.pageY - Q.input.offsetY;
|
||||
}
|
||||
|
||||
if(stage) {
|
||||
mouseMoveObj.x= Q.canvasToStageX(posX,stage);
|
||||
mouseMoveObj.y= Q.canvasToStageY(posY,stage);
|
||||
|
||||
Q.inputs[mouseInputX] = mouseMoveObj.x;
|
||||
Q.inputs[mouseInputY] = mouseMoveObj.y;
|
||||
|
||||
Q.input.trigger('mouseMove',mouseMoveObj);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fired when the user scrolls the mouse wheel up or down
|
||||
* Anyone subscribing to the "mouseWheel" event will receive an event with one numeric parameter
|
||||
* indicating the scroll direction. -1 for down, 1 for up.
|
||||
* @private
|
||||
*/
|
||||
Q._mouseWheel = function(e) {
|
||||
// http://www.sitepoint.com/html5-javascript-mouse-wheel/
|
||||
// cross-browser wheel delta
|
||||
e = window.event || e; // old IE support
|
||||
var delta = Math.max(-1, Math.min(1, (e.wheelDelta || -e.detail)));
|
||||
Q.input.trigger('mouseWheel', delta);
|
||||
};
|
||||
|
||||
Q.el.addEventListener('mousemove',Q._mouseMove,true);
|
||||
Q.el.addEventListener('touchstart',Q._mouseMove,true);
|
||||
Q.el.addEventListener('touchmove',Q._mouseMove,true);
|
||||
Q.el.addEventListener('mousewheel',Q._mouseWheel,true);
|
||||
Q.el.addEventListener('DOMMouseScroll',Q._mouseWheel,true);
|
||||
},
|
||||
|
||||
/**
|
||||
* Turn off mouse controls
|
||||
*
|
||||
* @method disableMouseControls
|
||||
* @for Q.InputSystem
|
||||
*/
|
||||
disableMouseControls: function() {
|
||||
if(Q._mouseMove) {
|
||||
Q.el.removeEventListener("mousemove",Q._mouseMove, true);
|
||||
Q.el.removeEventListener("mousewheel",Q._mouseWheel, true);
|
||||
Q.el.removeEventListener("DOMMouseScroll",Q._mouseWheel, true);
|
||||
Q.el.style.cursor = 'inherit';
|
||||
Q._mouseMove = null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Draw the touch buttons on the screen
|
||||
*
|
||||
* overload this to change how buttons are drawn
|
||||
*
|
||||
* @method drawButtons
|
||||
* @for Q.InputSystem
|
||||
*/
|
||||
drawButtons: function() {
|
||||
var keypad = Q.input.keypad,
|
||||
ctx = Q.ctx;
|
||||
|
||||
ctx.save();
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
|
||||
for(var i=0;i<keypad.controls.length;i++) {
|
||||
var control = keypad.controls[i];
|
||||
|
||||
if(control[0]) {
|
||||
ctx.font = "bold " + (keypad.size/2) + "px arial";
|
||||
var x = keypad.left + i * keypad.unit + keypad.gutter,
|
||||
y = keypad.bottom - keypad.unit,
|
||||
key = Q.inputs[control[0]];
|
||||
|
||||
ctx.fillStyle = keypad.color || "#FFFFFF";
|
||||
ctx.globalAlpha = key ? 1.0 : 0.5;
|
||||
ctx.fillRect(x,y,keypad.size,keypad.size);
|
||||
|
||||
ctx.fillStyle = keypad.text || "#000000";
|
||||
ctx.fillText(control[1],
|
||||
x+keypad.size/2,
|
||||
y+keypad.size/2);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
},
|
||||
|
||||
drawCircle: function(x,y,color,size) {
|
||||
var ctx = Q.ctx,
|
||||
joypad = Q.joypad;
|
||||
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.globalAlpha=joypad.alpha;
|
||||
ctx.fillStyle = color;
|
||||
ctx.arc(x, y, size, 0, Math.PI*2, true);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
},
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Draw the joypad on the screen
|
||||
*
|
||||
* overload this to change how joypad is drawn
|
||||
*
|
||||
* @method drawJoypad
|
||||
* @for Q.InputSystem
|
||||
*/
|
||||
drawJoypad: function() {
|
||||
var joypad = Q.joypad;
|
||||
if(joypad.joypadTouch !== null) {
|
||||
Q.input.drawCircle(joypad.centerX,
|
||||
joypad.centerY,
|
||||
joypad.background,
|
||||
joypad.size);
|
||||
|
||||
if(joypad.x !== null) {
|
||||
Q.input.drawCircle(joypad.x,
|
||||
joypad.y,
|
||||
joypad.color,
|
||||
joypad.center);
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Called each frame by the stage game loop to render any onscreen UI
|
||||
*
|
||||
* calls `drawJoypad` and `drawButtons` if enabled
|
||||
*
|
||||
* @method drawCanvas
|
||||
* @for Q.InputSystem
|
||||
*/
|
||||
drawCanvas: function() {
|
||||
if(this.touchEnabled) {
|
||||
this.drawButtons();
|
||||
}
|
||||
|
||||
if(this.joypadEnabled) {
|
||||
this.drawJoypad();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
|
||||
/**
|
||||
* Instance of the input subsytem that is actually used during gameplay
|
||||
*
|
||||
* @property Q.input
|
||||
* @for Quintus.Input
|
||||
* @type Q.InputSystem
|
||||
*/
|
||||
Q.input = new Q.InputSystem();
|
||||
|
||||
/**
|
||||
* Helper method to activate controls with default options
|
||||
*
|
||||
* @for Quintus.Input
|
||||
* @method Q.controls
|
||||
* @param {Boolean} joypad - enable 4-way joypad (true) or just left, right controls (false, undefined)
|
||||
*/
|
||||
Q.controls = function(joypad) {
|
||||
Q.input.keyboardControls();
|
||||
|
||||
if(joypad) {
|
||||
Q.input.touchControls({
|
||||
controls: [ [],[],[],['action','b'],['fire','a']]
|
||||
});
|
||||
Q.input.joypadControls();
|
||||
} else {
|
||||
Q.input.touchControls();
|
||||
}
|
||||
|
||||
return Q;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Platformer Control Component
|
||||
*
|
||||
* Adds 2D platformer controls onto a Sprite
|
||||
*
|
||||
* Platformer controls bind to left, and right and allow the player to jump.
|
||||
*
|
||||
* Adds the following properties to the entity to control speed and jumping:
|
||||
*
|
||||
* {
|
||||
* speed: 200,
|
||||
* jumpSpeed: -300
|
||||
* }
|
||||
*
|
||||
*
|
||||
* @class platformerControls
|
||||
* @for Quintus.Input
|
||||
*/
|
||||
Q.component("platformerControls", {
|
||||
defaults: {
|
||||
speed: 200,
|
||||
jumpSpeed: -300,
|
||||
collisions: []
|
||||
},
|
||||
|
||||
added: function() {
|
||||
var p = this.entity.p;
|
||||
|
||||
Q._defaults(p,this.defaults);
|
||||
|
||||
this.entity.on("step",this,"step");
|
||||
this.entity.on("bump.bottom",this,"landed");
|
||||
|
||||
p.landed = 0;
|
||||
p.direction ='right';
|
||||
},
|
||||
|
||||
landed: function(col) {
|
||||
var p = this.entity.p;
|
||||
p.landed = 1/5;
|
||||
},
|
||||
|
||||
step: function(dt) {
|
||||
var p = this.entity.p;
|
||||
|
||||
if(p.ignoreControls === undefined || !p.ignoreControls) {
|
||||
var collision = null;
|
||||
|
||||
// Follow along the current slope, if possible.
|
||||
if(p.collisions !== undefined && p.collisions.length > 0 && (Q.inputs['left'] || Q.inputs['right'] || p.landed > 0)) {
|
||||
if(p.collisions.length === 1) {
|
||||
collision = p.collisions[0];
|
||||
} else {
|
||||
// If there's more than one possible slope, follow slope with negative Y normal
|
||||
collision = null;
|
||||
|
||||
for(var i = 0; i < p.collisions.length; i++) {
|
||||
if(p.collisions[i].normalY < 0) {
|
||||
collision = p.collisions[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Don't climb up walls.
|
||||
if(collision !== null && collision.normalY > -0.3 && collision.normalY < 0.3) {
|
||||
collision = null;
|
||||
}
|
||||
}
|
||||
|
||||
if(Q.inputs['left']) {
|
||||
p.direction = 'left';
|
||||
if(collision && p.landed > 0) {
|
||||
p.vx = p.speed * collision.normalY;
|
||||
p.vy = -p.speed * collision.normalX;
|
||||
} else {
|
||||
p.vx = -p.speed;
|
||||
}
|
||||
} else if(Q.inputs['right']) {
|
||||
p.direction = 'right';
|
||||
if(collision && p.landed > 0) {
|
||||
p.vx = -p.speed * collision.normalY;
|
||||
p.vy = p.speed * collision.normalX;
|
||||
} else {
|
||||
p.vx = p.speed;
|
||||
}
|
||||
} else {
|
||||
p.vx = 0;
|
||||
if(collision && p.landed > 0) {
|
||||
p.vy = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if(p.landed > 0 && (Q.inputs['up'] || Q.inputs['action']) && !p.jumping) {
|
||||
p.vy = p.jumpSpeed;
|
||||
p.landed = -dt;
|
||||
p.jumping = true;
|
||||
} else if(Q.inputs['up'] || Q.inputs['action']) {
|
||||
this.entity.trigger('jump', this.entity);
|
||||
p.jumping = true;
|
||||
}
|
||||
|
||||
if(p.jumping && !(Q.inputs['up'] || Q.inputs['action'])) {
|
||||
p.jumping = false;
|
||||
this.entity.trigger('jumped', this.entity);
|
||||
if(p.vy < p.jumpSpeed / 3) {
|
||||
p.vy = p.jumpSpeed / 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
p.landed -= dt;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Step Controls component
|
||||
*
|
||||
* Adds Step (square grid based) 4-ways controls onto a Sprite
|
||||
*
|
||||
* Adds the following properties to the entity:
|
||||
*
|
||||
* {
|
||||
* stepDistance: 32, // should be tile size
|
||||
* stepDelay: 0.2 // seconds to delay before next step
|
||||
* }
|
||||
*
|
||||
*
|
||||
* @class stepControls
|
||||
* @for Quintus.Input
|
||||
*/
|
||||
Q.component("stepControls", {
|
||||
|
||||
added: function() {
|
||||
var p = this.entity.p;
|
||||
|
||||
if(!p.stepDistance) { p.stepDistance = 32; }
|
||||
if(!p.stepDelay) { p.stepDelay = 0.2; }
|
||||
|
||||
p.stepWait = 0;
|
||||
this.entity.on("step",this,"step");
|
||||
this.entity.on("hit", this,"collision");
|
||||
},
|
||||
|
||||
collision: function(col) {
|
||||
var p = this.entity.p;
|
||||
|
||||
if(p.stepping) {
|
||||
p.stepping = false;
|
||||
p.x = p.origX;
|
||||
p.y = p.origY;
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
step: function(dt) {
|
||||
var p = this.entity.p,
|
||||
moved = false;
|
||||
p.stepWait -= dt;
|
||||
|
||||
if(p.stepping) {
|
||||
p.x += p.diffX * dt / p.stepDelay;
|
||||
p.y += p.diffY * dt / p.stepDelay;
|
||||
}
|
||||
|
||||
if(p.stepWait > 0) { return; }
|
||||
if(p.stepping) {
|
||||
p.x = p.destX;
|
||||
p.y = p.destY;
|
||||
}
|
||||
p.stepping = false;
|
||||
|
||||
p.diffX = 0;
|
||||
p.diffY = 0;
|
||||
|
||||
if(Q.inputs['left']) {
|
||||
p.diffX = -p.stepDistance;
|
||||
} else if(Q.inputs['right']) {
|
||||
p.diffX = p.stepDistance;
|
||||
}
|
||||
|
||||
if(Q.inputs['up']) {
|
||||
p.diffY = -p.stepDistance;
|
||||
} else if(Q.inputs['down']) {
|
||||
p.diffY = p.stepDistance;
|
||||
}
|
||||
|
||||
if(p.diffY || p.diffX ) {
|
||||
p.stepping = true;
|
||||
p.origX = p.x;
|
||||
p.origY = p.y;
|
||||
p.destX = p.x + p.diffX;
|
||||
p.destY = p.y + p.diffY;
|
||||
p.stepWait = p.stepDelay;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
};
|
||||
|
||||
if(typeof Quintus === 'undefined') {
|
||||
module.exports = quintusInput;
|
||||
} else {
|
||||
quintusInput(Quintus);
|
||||
}
|
||||
Reference in New Issue
Block a user