マウス操作を改善するために、Pointer Lock APIを使ってみた。
Pointer Lock APIを使うとマウス座標が移動量で検出できて、マウスカーソルも消える。おかげでパドルコントロールにおけるストレスがかなり解消された。しかし依然として面白くはない。これは一人スカッシュという遊びそのものが面白くないせいかもしれないけども、改良すればもう少し面白くできるのではないかと思う。
で、改良のための参考として2人用だけどPongを観てみると、ボールの跳ね返す角度が、パドルでコントロールできるようになっている。
ボールをパドルでコントロールできれば楽しそうだ。次はそれを実装することにしよう。
YoutubeでPongの動画をチェックしていると下の古い動画を見つけた。ボールの動きが可笑しいね。。
動作サンプル
ソースコード・リソース
### Pointer Lock APIを使ってみる
* Pointer Lock APIを使って、マウスを占有してみた。
* ESCを押すとPointer Lockを停止します。
* EdgeではPointer Lock APIが未実装のため、Pointer Lockできません。
* iPhoneでは今のところ動作しません。理由は不明ですが、おそらくES6 Generatorを使っているせいだと思われます。
<!DOCTYPE html>
<html>
<head>
<title>スカッシュゲームを作る - Pointer Lock APIを使用してみる。</title>
<meta name="keywords" content="WebGL,HTML5,three.js" />
<meta name="description" content="WebGL,HTML5,three.js" />
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no" />
<meta charset="UTF-8">
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r71/three.js"></script>
<style>
html {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
body {
width: 100%;
height: 100%;
margin: 4px;
padding: 0;
border: 0;
text-align: center;
margin-left: auto;
margin-right: auto;
}
#console {
margin-left: auto;
margin-right: auto;
border: 0;
padding: 0;
}
</style>
</head>
<body>
<div id="content"></div>
<script type="text/javascript">
const ASCII_CHARS = [
[
0,0,0,0,0,
0,0,0,0,0,
0,0,0,0,0,
0,0,0,0,0,
0,0,0,0,0
],
[
0,0,1,0,0,
0,0,1,0,0,
0,0,1,0,0,
0,0,0,0,0,
0,0,1,0,0
],
[
0,1,0,1,0,
0,1,0,1,0,
0,0,0,0,0,
0,0,0,0,0,
0,0,0,0,0
],
[
0,1,0,1,0,
1,1,1,1,1,
0,1,0,1,0,
1,1,1,1,1,
0,1,0,1,0
],
[
1,1,1,1,1,
1,0,1,0,0,
1,1,1,1,1,
0,0,1,0,1,
1,1,1,1,1
],
[
1,1,0,0,1,
1,1,0,1,0,
0,0,1,0,0,
0,1,0,1,1,
1,0,0,1,1
],
[
1,1,1,0,0,
1,0,1,0,0,
0,1,1,0,1,
1,0,0,1,0,
1,1,1,0,1
],
[
0,0,1,0,0,
0,0,1,0,0,
0,0,0,0,0,
0,0,0,0,0,
0,0,0,0,0
],
[
0,0,1,1,0,
0,1,0,0,0,
0,1,0,0,0,
0,1,0,0,0,
0,0,1,1,0
],
[
0,1,1,0,0,
0,0,0,1,0,
0,0,0,1,0,
0,0,0,1,0,
0,1,1,0,0
],
[
1,0,1,0,1,
0,1,1,1,0,
1,1,1,1,1,
0,1,1,1,0,
1,0,1,0,1
],
[
0,0,1,0,0,
0,0,1,0,0,
1,1,1,1,1,
0,0,1,0,0,
0,0,1,0,0
],
[
0,0,0,0,0,
0,0,0,0,0,
0,0,0,0,0,
0,0,1,0,0,
0,1,0,0,0
],
[
0,0,0,0,0,
0,0,0,0,0,
1,1,1,1,1,
0,0,0,0,0,
0,0,0,0,0
],
[
0,0,0,0,0,
0,0,0,0,0,
0,0,0,0,0,
0,0,0,0,0,
0,0,1,0,0
],
[
0,0,0,0,1,
0,0,0,1,0,
0,0,1,0,0,
0,1,0,0,0,
1,0,0,0,0
],
[1,1,1,1,1,
1,1,0,0,1,
1,0,1,0,1,
1,0,0,1,1,
1,1,1,1,1],
[0,1,1,0,0,
0,0,1,0,0,
0,0,1,0,0,
0,0,1,0,0,
0,1,1,1,0],
[1,1,1,1,1,
1,0,0,0,1,
0,0,1,1,0,
0,1,0,0,0,
1,1,1,1,1],
[1,1,1,1,1,
0,0,0,0,1,
1,1,1,1,1,
0,0,0,0,1,
1,1,1,1,1],
[1,0,0,1,0,
1,0,0,1,0,
1,0,0,1,0,
1,1,1,1,1,
0,0,0,1,0],
[1,1,1,1,1,
1,0,0,0,0,
1,1,1,1,1,
0,0,0,0,1,
1,1,1,1,1],
[1,1,1,1,1,
1,0,0,0,0,
1,1,1,1,1,
1,0,0,0,1,
1,1,1,1,1],
[1,1,1,1,1,
1,0,0,1,0,
0,0,1,0,0,
0,1,0,0,0,
1,0,0,0,0],
[1,1,1,1,1,
1,0,0,0,1,
1,1,1,1,1,
1,0,0,0,1,
1,1,1,1,1],
[1,1,1,1,1,
1,0,0,0,1,
1,1,1,1,1,
0,0,0,0,1,
1,1,1,1,1],
[
0,0,0,0,0,
0,0,1,0,0,
0,0,0,0,0,
0,0,1,0,0,
0,0,0,0,0
],
[
0,0,0,0,0,
0,0,1,0,0,
0,0,0,0,0,
0,0,1,0,0,
0,1,0,0,0
],
[
0,0,0,1,0,
0,0,1,0,0,
0,1,0,0,0,
0,0,1,0,0,
0,0,0,1,0
],
[
0,0,0,0,0,
1,1,1,1,1,
0,0,0,0,0,
1,1,1,1,1,
0,0,0,0,0
],
[
0,1,0,0,0,
0,0,1,0,0,
0,0,0,1,0,
0,0,1,0,0,
0,1,0,0,0
],
[
1,1,1,1,1,
1,0,0,0,1,
0,0,1,1,1,
0,0,0,0,0,
0,0,1,0,0
],
[
0,1,1,1,0,
1,0,0,0,1,
1,0,1,1,1,
1,0,0,0,0,
0,1,1,1,1
],
[
1,1,1,1,1,
1,0,0,0,1,
1,1,1,1,1,
1,0,0,0,1,
1,0,0,0,1
],
[
1,1,1,1,1,
1,0,0,0,1,
1,1,1,1,0,
1,0,0,0,1,
1,1,1,1,1
],
[
1,1,1,1,1,
1,0,0,0,0,
1,0,0,0,0,
1,0,0,0,0,
1,1,1,1,1
],
[
1,1,1,1,0,
1,0,0,0,1,
1,0,0,0,1,
1,0,0,0,1,
1,1,1,1,0
],
[
1,1,1,1,1,
1,0,0,0,0,
1,1,1,1,1,
1,0,0,0,0,
1,1,1,1,1
],
[
1,1,1,1,1,
1,0,0,0,0,
1,1,1,1,1,
1,0,0,0,0,
1,0,0,0,0
],
[
1,1,1,1,1,
1,0,0,0,0,
1,0,1,1,1,
1,0,0,0,1,
1,1,1,1,1
],
[
1,0,0,0,1,
1,0,0,0,1,
1,1,1,1,1,
1,0,0,0,1,
1,0,0,0,1
],
[
0,0,1,0,0,
0,0,1,0,0,
0,0,1,0,0,
0,0,1,0,0,
0,0,1,0,0
],
[
1,1,1,1,1,
0,0,1,0,0,
0,0,1,0,0,
1,0,1,0,0,
1,1,1,0,0
],
[
1,0,0,,1,
1,0,0,1,0,
1,1,1,0,0,
1,0,0,1,0,
1,0,0,0,1
],
[
1,0,0,0,0,
1,0,0,0,0,
1,0,0,0,0,
1,0,0,0,0,
1,1,1,1,1
],
[
1,1,0,1,1,
1,0,1,0,1,
1,0,0,0,1,
1,0,0,0,1,
1,0,0,0,1
],
[
1,0,0,0,1,
1,1,0,0,1,
1,0,1,0,1,
1,0,0,1,1,
1,0,0,0,1
],
[
1,1,1,1,1,
1,0,0,0,1,
1,0,0,0,1,
1,0,0,0,1,
1,1,1,1,1
],
[
1,1,1,1,1,
1,0,0,0,1,
1,1,1,1,1,
1,0,0,0,0,
1,0,0,0,0
],
[
1,1,1,1,1,
1,0,0,0,1,
1,0,0,0,1,
1,0,0,1,1,
1,1,1,1,1
],
[
1,1,1,1,1,
1,0,0,0,1,
1,1,1,1,0,
1,0,0,0,1,
1,0,0,0,1
],
[
1,1,1,1,1,
1,0,0,0,0,
1,1,1,1,1,
0,0,0,0,1,
1,1,1,1,1
],
[
1,1,1,1,1,
0,0,1,0,0,
0,0,1,0,0,
0,0,1,0,0,
0,0,1,0,0
],
[
1,0,0,0,1,
1,0,0,0,1,
1,0,0,0,1,
1,0,0,0,1,
1,1,1,1,1
],
[
1,0,0,0,1,
1,0,0,0,1,
0,1,0,1,0,
0,1,0,1,0,
0,0,1,0,0
],
[
1,0,1,0,1,
1,0,1,0,1,
1,0,1,0,1,
1,0,1,0,1,
1,1,1,1,1
],
[
1,0,0,0,1,
0,1,0,1,0,
0,0,1,0,0,
0,1,0,1,0,
1,0,0,0,1
],
[
1,0,0,0,1,
0,1,0,1,0,
0,0,1,0,0,
0,0,1,0,0,
0,0,1,0,0
],
[
1,1,1,1,1,
0,0,0,1,0,
0,0,1,0,0,
0,1,0,0,0,
1,1,1,1,1
]
];
window.addEventListener('load',
function () {
'use strict';
const WIDTH = 192;
const HEIGHT = 256;
var screen_width;
var screen_height;
var remain = 3;
var score = 0;
var renderer;
var x = 0;
var y = 0;
var dx = 3;
var dy = 3;
var px;// paddle x pos
function calcScreenSize() {
screen_width = document.body.clientWidth - 8;
screen_height = document.body.clientHeight - 8;
if (screen_width >= screen_height) {
screen_width = screen_height * WIDTH / HEIGHT;
} else {
screen_height = screen_width * HEIGHT / WIDTH;
}
}
calcScreenSize();
renderer = new THREE.WebGLRenderer({ antialias: false /*, sortObjects: true */ });
renderer.setSize(screen_width, screen_height);
renderer.setClearColor(0x000000, 1);
renderer.domElement.id = 'console';
renderer.domElement.style.zIndex = 0;
document.body.appendChild(renderer.domElement);
renderer.clear();
// Pointer Lock API
var isPointerLocked = false;
var isPointerRequesting = false;
var elm = renderer.domElement;//document.body;
function pointerLockChange() {
if (
document.pointerLockElement === elm ||
document.mozPointerLockElement === elm ||
document.webkitPointerLockElement === elm) {
isPointerLocked = true;
} else {
isPointerLocked = false;
}
isPointerRequesting = false;
}
document.addEventListener('pointerlockchange',pointerLockChange,false);
document.addEventListener('mozpointerlockchange',pointerLockChange,false);
document.addEventListener('webkitpointerlockchange',pointerLockChange,false);
elm.requestPointerLock = elm.requestPointerLock ||
elm.mozRequestPointerLock ||
elm.webkitRequestPointerLock;
// カメラを工夫し、Z座標が0の時座標指定が仮想画面サイズの位置となるようにする
var camera = new THREE.PerspectiveCamera(90, WIDTH / HEIGHT, 0.1, 1000);
camera.position.z = HEIGHT / 2;
var scene = new THREE.Scene();
var geometry = new THREE.PlaneBufferGeometry(4, 4);
var material = new THREE.MeshBasicMaterial({ color: 0xffffff });
var ball = new THREE.Mesh(geometry, material);
var paddle = new THREE.Mesh(new THREE.PlaneBufferGeometry(32, 4), new THREE.MeshBasicMaterial({ color: 0xffffff }));
paddle.position.y = -100;
// 文字コード -> mesh 変換
var asciiCharObjs4 = [];
var asciiCharObjs2 = [];
var asciiGeometry2 = new THREE.PlaneBufferGeometry(2, 2);
var asciiGeometry4 = new THREE.PlaneBufferGeometry(4,4);
for(var i = 0,l = ASCII_CHARS.length;i < l;++i ){
var c2 = new THREE.Object3D();
var c4 = new THREE.Object3D();
asciiCharObjs2.push(c2);
asciiCharObjs4.push(c4);
for(var cy = 0;cy < 5;++cy){
for(var cx = 0;cx < 5;++cx){
if(ASCII_CHARS[i][cy * 5 + cx]){
var mesh = new THREE.Mesh(asciiGeometry2,material);
mesh.position.x = cx * 2;
mesh.position.y = 10 - cy * 2;
c2.add(mesh);
var mesh = new THREE.Mesh(asciiGeometry4,material);
mesh.position.x = cx * 4;
mesh.position.y = 10 - cy * 4;
c4.add(mesh);
}
}
}
}
function createStringMesh(str,size){
if(!size) size = 2;
var strObj = new THREE.Object3D();
var asciiChars = size == 4?asciiCharObjs4:asciiCharObjs2;
for(var i = 0,l = str.length;i < l;++i){
var sx = i * 6 * size;
var c = str.charCodeAt(i) - 0x20;
var co = asciiChars[c].clone();
co.position.x = sx;
strObj.add(co);
}
return strObj;
}
// PRESS_MOUSE 文字列
const PRESS_MOUSE = 'PRESS MOUSE BTN';
var pressMouse = createStringMesh(PRESS_MOUSE);
pressMouse.position.x = - PRESS_MOUSE.length * 2 * 6 / 2;
pressMouse.position.y = 0;
scene.add(pressMouse);
// Title
const TITLE = 'SQUASH';
var titleObj = createStringMesh(TITLE,4);
titleObj.position.x = - TITLE.length * 4 * 6 / 2;
titleObj.position.y = 70;
scene.add(titleObj);
// GAME OVER
const GAME_OVER = 'GAME OVER';
var gameOverObj = createStringMesh(GAME_OVER,2);
gameOverObj.position.x = - GAME_OVER.length * 2 * 6 / 2;
gameOverObj.position.y = 40;
scene.add(gameOverObj);
// スコア表示用
var scoreObj = new THREE.Object3D();
for(var i = 0;i < 5;++i){
var sx = i * 6 * 2;
var digit = new THREE.Object3D();
scoreObj.add(digit);
for(var j = 0;j < 10;++j){
var n = asciiCharObjs2[0x10 + j].clone();
n.position.x = sx;
n.visible = false;
digit.add(n);
}
}
scoreObj.position.y = 110;
scoreObj.position.x = - 6 * 2 * 5 / 2;
scoreObj.children[0].children[0].visible = true;
scoreObj.children[1].children[0].visible = true;
scoreObj.children[2].children[0].visible = true;
scoreObj.children[3].children[0].visible = true;
scoreObj.children[4].children[0].visible = true;
scene.add(scoreObj);
var scoreBackup = score;
function updateScore(){
if(score > 99999){
score = 99999;
}
var c5 = parseInt(score / 10000) % 10;
var c4 = parseInt(score / 1000) % 10;
var c3 = parseInt(score / 100) % 10;
var c2 = parseInt(score / 10) % 10;
var c1 = parseInt(score) % 10;
var b5 = parseInt(scoreBackup / 10000) % 10;
var b4 = parseInt(scoreBackup / 1000) % 10;
var b3 = parseInt(scoreBackup / 100) % 10;
var b2 = parseInt(scoreBackup / 10) % 10;
var b1 = parseInt(scoreBackup) % 10;
scoreObj.children[0].children[b5].visible = false;
scoreObj.children[0].children[c5].visible = true;
scoreObj.children[1].children[b4].visible = false;
scoreObj.children[1].children[c4].visible = true;
scoreObj.children[2].children[b3].visible = false;
scoreObj.children[2].children[c3].visible = true;
scoreObj.children[3].children[b2].visible = false;
scoreObj.children[3].children[c2].visible = true;
scoreObj.children[4].children[b1].visible = false;
scoreObj.children[4].children[c1].visible = true;
scoreBackup = score;
}
// 残数表示
var remainObj = new THREE.Object3D();
for(var j = 0;j < 10;++j){
var n = asciiCharObjs2[j + 0x10].clone();
n.visible = false;
remainObj.add(n);
}
remainObj.position.y = -124;
remainObj.position.x = 70;
var remainBackup = 0;
function updateRemain(){
remainObj.children[remain].visible = true;
remainObj.children[remainBackup].visible = false;
remainBackup = remain;
}
scene.add(remainObj);
elm.addEventListener('mousemove', function (e) {
if(isPointerLocked){
var movementX = e.movementX ||
e.mozMovementX ||
e.webkitMovementX ||
0,
movementY = e.movementY ||
e.mozMovementY ||
e.webkitMovementY ||
0;
px += movementX;
if(px < ( -WIDTH / 2)) px = - WIDTH / 2;
if(px > ( WIDTH / 2)) px = WIDTH / 2;
} else {
var ex = e.clientX;
var ey = e.clientY;
var rect = e.target.getBoundingClientRect();
ex -= rect.left;
ey -= rect.top;
px = ex * WIDTH / screen_width - WIDTH / 2;
}
//paddle.position.x = x * WIDTH / screen_width - WIDTH / 2;
});
var click = false;
elm.addEventListener('click',function(){
click = true;
if((!isPointerLocked) && elm.requestPointerLock){
isPointerRequesting = true;
elm.requestPointerLock();
} else {
isPointerRequesting = false;
}
});
function mouseCheck(){
var ret = click;
click = false;
return ret;
}
window.addEventListener('resize', function () {
calcScreenSize();
renderer.setSize(screen_width, screen_height);
});
scene.add(ball);
scene.add(paddle);
// ジェネレータによるゲームメインの実装
function* game(){
while(true){
// init
remain = 3;
updateRemain();
x = 0;
y = 0;
dx = 2;
dy = 2;
click = false;
titleObj.visible = true;
paddle.visible = false;
ball.visible = false;
pressMouse.visible = true;
remainObj.visible = false;
gameOverObj.visible = false;
// game start wait
var start = false;
while(!mouseCheck() && !start){
for(var i = 0;i < 10;++i ){
if(mouseCheck()){
start = true;
while(isPointerRequesting){
yield;
}
break;
}
yield;
}
pressMouse.visible = !pressMouse.visible;
}
score = 0;
updateScore();
titleObj.visible = false;
pressMouse.visible = false;
paddle.visible = true;
ball.visible = true;
remainObj.visible = true;
// game play
while(remain > 0){
if(!play()){
x = 0;
y = 0;
dx = Math.abs(dx);
dy = Math.abs(dy);
remain--;
updateRemain();
} else {
yield;
};
}
// game over
gameOverObj.visible = true;
for(var i = 0;i < 5 * 20;++i){
yield;
}
gameOverObj.visible =false;
continue;
}
}
function play(){
// ボールの動き
var bx = x, by = y;
x += dx;
y += dy;
if (x > (WIDTH / 2) || x < (-WIDTH / 2)) {
dx = -dx;
x += dx;
}
if (y > (HEIGHT / 2) ) {
dy = -dy;
y += dy;
}
if(y < (-HEIGHT / 2)){
return false;
}
ball.position.x = x;
ball.position.y = y;
// パドルとの衝突判定
var sx, sy, ex, ey;
if (x >= bx) {
sx = bx - 2;
ex = x + 2;
} else {
sx = x - 2;
ex = bx + 2;
}
if (y <= by) {
sy = by - 2;
ey = y + 2;
} else {
sy = y - 2;
ey = by + 2;
}
paddle.position.x = px;
var psx = paddle.position.x - 16, pex = paddle.position.x + 16, psy = paddle.position.y - 2, pey = paddle.position.y + 2;
if (sy <= pey && psy <= ey && sx <= pex && psx <= sx) {
var cx = -100 * dy / dx;
var cy = -100;
y += 2;
dy = -dy;
y += dy;
++score;
// 徐々に難易度を上げていく
if (dx < 3.8){
dx = Math.sign(dx) * (Math.abs(dx) + 0.025);
dy = Math.sign(dy) * (Math.abs(dy) + 0.025);
}
updateScore();
}
return true;
}
//
var g = game();
function render() {
requestAnimationFrame(render);
renderer.render(scene, camera);
g.next();
}
render();
});
</script>
</body>
</html>