freeball
Version:
Soccer with balls that can fly
504 lines (431 loc) • 19.2 kB
JavaScript
/* global requirejs */
// client-side THREE.JS viewer of the game
var v;
requirejs(
['three', 'Stats', 'js/types', 'js/server', 'js/keyboardCommands', 'optional!socketio', 'pointerLockControls', 'autoCameraControls','trackballControls'],
function(THREE, Stats, types, server, keyboardCommands, socketio, pointerLockControls, autoCameraControls ) {
var bodyTypes = types.bodyTypes;
var socket;
var thisPlayerId = null;
var focusPlayer = null;
var activeControls = null;
var tweenSpeed = 0.6;
// `self` is `window` unless you're in a worker
var now = ( self.performance && self.performance.now ) ? self.performance.now.bind( performance ) : Date.now;
var rendererDivId = "renderer";
var colors = {};
colors.green = 0x99ff99;
colors.red = 0xff9999;
colors.yellow = 0xffff99;
colors.blue = 0x9999ff;
colors.skyBlue = 0xddddff;
colors.darkNight = 0x280137;
colors.white = 0xffffff;
var redMaterial = new THREE.MeshLambertMaterial( { color: colors.red } );
var blueMaterial = new THREE.MeshLambertMaterial( { color: colors.blue } );
var popperMaterials = {
0: new THREE.MeshLambertMaterial( { color: colors.blue, transparent: true, opacity: 0.5 } ),
1: new THREE.MeshLambertMaterial( { color: colors.red, transparent: true, opacity: 0.5 } )
}
var yellowMaterial = new THREE.MeshLambertMaterial( { color: colors.yellow } );
var ambientLight = new THREE.AmbientLight( 0x555555 );
//var earthTexture = new THREE.TextureLoader().load( 'images/land_ocean_ice_cloud_2048.jpg' );
var stats;
var createTickStats = function() {
var tickDiv = stats.createPanel( 'tick', '#ff0', '#020' );
var tickText = tickDiv.children[ 0 ];
var tickGraph = tickDiv.children[ 1 ];
}
var initStats = function() {
stats = new Stats();
stats.setMode( 0 ); // 0: fps, 1: ms, 2: mb
// align top-left
stats.domElement.style.position = 'absolute';
stats.domElement.style.left = '0px';
stats.domElement.style.top = '0px';
//stats.domElement.
document.body.appendChild( stats.domElement );
};
initStats();
function onWindowResize() {
v.camera.aspect = window.innerWidth / window.innerHeight;
v.camera.updateProjectionMatrix();
v.renderer.setSize( window.innerWidth, window.innerHeight );
};
var initTrackball = function(camera, renderer) {
var trackBall = new THREE.trackballControls( camera );
trackBall.rotateSpeed = 5.0;
trackBall.zoomSpeed = 2.0;
trackBall.panSpeed = 2.0;
trackBall.noZoom = false;
trackBall.noPan = false;
trackBall.staticMoving = true;
trackBall.dynamicDampingFactor = 0.3;
trackBall.keys = [ 65, 83, 68 ];
//controls.addEventListener( 'change', renderer );
return trackBall;
}
var controls;
var initScene = function() {
var viewer = {};
var objects = [];
var playerMeshes = [];
var recentServerPositions = [];
var recentServerVelocities = [];
var recentTickTimestamp = 0;
viewer.objects = objects;
var camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );
//camera.position.set(1, 1, 1);
//camera.lookAt(new THREE.Vector3([0,0,0]));
viewer.camera = camera;
viewer.audioListener = new THREE.AudioListener();
viewer.camera.add( viewer.audioListener );
viewer.scene = new THREE.Scene();
var scene = viewer.scene;
scene.fog = new THREE.Fog( colors.darkNight, 0, 500 );
if (true) {
activeControls = autoCameraControls;
controls = new autoCameraControls.autoCameraControls(camera, scene);
humane.log('<strong>WASD</strong>=move, <strong>L</strong>=dive, <strong>P</strong>=fly.');
if (socketio === undefined) {
humane.log('Pop the ball on your colored popper to score.');
humane.log('To play multiplayer `npm install -g freeball` or <a href="https://github.com/ubershmekel/freeball">git clone</a>')
}
} else {
if (pointerLockControls.supportsPointerLock) {
activeControls = pointerLockControls;
controls = new pointerLockControls.pointerLockControls(camera);
pointerLockControls.requirePointerLock();
scene.add(controls.object);
camera.up = new THREE.Vector3(0,0,1);
camera.lookAt(new THREE.Vector3(0,0,0));
} else {
activeControls = trackballControls;
controls = initTrackball(camera, renderer);
scene.add(camera);
}
}
viewer.controls = controls;
scene.add( ambientLight );
var lights = [
{
pos: [0, 0, 100],
color: colors.white
},
{
pos: [0, -130, 100],
color: colors.blue
},
{
pos: [0, 130, 100],
color: colors.red
}
];
lights.forEach(function(lite) {
var light = new THREE.SpotLight( lite.color );
var pos = lite.pos;
//light.intensity = 0.9;
light.position.set( pos[0], pos[1], pos[2] );
light.target.position.set( pos[0], pos[1], 0 );
light.castShadow = true;
light.shadow.camera.near = 10;
light.shadow.camera.far = 250;
light.shadow.camera.fov = 70;
light.shadowMapBias = 0.1;
light.shadowMapDarkness = 0.7;
light.shadow.mapSize.Width = 2*512;
light.shadow.mapSize.Height = 2*512;
//scene.add(new THREE.CameraHelper( light.shadow.camera ));
scene.add( light );
scene.add( light.target );
viewer.light = light;
})
//viewer.light = light;
var renderer = new THREE.WebGLRenderer();
viewer.renderer = renderer;
renderer.shadowMap.enabled = true;
renderer.shadowMapSoft = true;
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.setClearColor( scene.fog.color, 1 );
renderer.domElement.id = rendererDivId;
document.body.appendChild( renderer.domElement );
function addBodyToScene(mesh, typ) {
objects.push({
mesh: mesh,
type: typ
});
scene.add(mesh);
console.log(typ);
}
function createPlayer (typeInfo) {
var colors = {0: blueMaterial, 1: redMaterial};
var playerGeometry = new THREE.SphereGeometry(typeInfo.radius, 32, 32);
var playerMesh = new THREE.Mesh( playerGeometry, colors[typeInfo.teamI] );
playerMesh.castShadow = true;
playerMesh.receiveShadow = true;
playerMesh.position.set(0, 0, 0);
playerMeshes.push(playerMesh);
addBodyToScene(playerMesh, bodyTypes.player);
};
var ballMesh = null;
function createSphere (typeInfo) {
var ballRadius = typeInfo.radius;
var ballGeometry = new THREE.SphereGeometry(ballRadius, 32, 32);
var material = yellowMaterial;
if(typeInfo.type == bodyTypes.popper) {
material = popperMaterials[typeInfo.team];
}
var ballMesh = new THREE.Mesh( ballGeometry, material );
ballMesh.castShadow = true;
ballMesh.receiveShadow = true;
ballMesh.position.set(0, 0, 0);
addBodyToScene(ballMesh, bodyTypes.ball);
return ballMesh;
};
function createPlane(typeInfo) {
// floor
var planeGeometry = new THREE.PlaneGeometry( 50, 400, 20, 20 );
planeGeometry.applyMatrix( new THREE.Matrix4().makeRotationFromQuaternion(typeInfo.quaternion) );
var material = new THREE.MeshLambertMaterial( {
color: colors.white,
wireframe: true,
//wireframeLinewidth does not work on windows: wireframeLinewidth: 2.0
//map: earthTexture
});
var mesh = new THREE.Mesh( planeGeometry, material );
mesh.castShadow = true;
mesh.receiveShadow = true;
addBodyToScene(mesh, bodyTypes.ground);
}
function clearScene(scene) {
var renderer = document.getElementById(rendererDivId);
renderer.parentNode.removeChild(renderer);
var i;
for(i=0; i < scene.children.length; i++){
var obj = scene.children[i];
scene.remove(obj);
}
}
var lastPaintTime = now();
viewer.onPaint = function() {
// on paint - hopefully faster than onServerTick
var dt = now() - lastPaintTime;
//predictPositions(objects, recentServerPositions, recentServerVelocities, recentTickTimestamp);
setPositions(objects, recentServerPositions);
controls.update(dt);
//controls.updateCommands();
renderer.render( scene, camera );
}
function setPositions(objects, dstPositions) {
for(var i = 0; i < objects.length; i++) {
var obj = objects[i].mesh;
var vec = dstPositions[i];
obj.position.x = vec[0];
obj.position.y = vec[1];
obj.position.z = vec[2];
}
}
function predictPositions(objects, srcPositions, velocities, lastUpdateTimestamp) {
//return;
var dt = (now() - lastUpdateTimestamp) / 1000.0;
for(var i = 0; i < objects.length; i++) {
var pos = objects[i].mesh.position;
var srcPos = srcPositions[i];
var vel = velocities[i];
pos.multiplyScalar(1 - tweenSpeed)
pos.x = pos.x + (srcPos[0] + vel[0] * dt) * tweenSpeed;
pos.y = pos.y + (srcPos[1] + vel[1] * dt) * tweenSpeed;
pos.z = pos.z + (srcPos[2] + vel[2] * dt) * tweenSpeed;
}
};
function updatePositions(positions, velocities) {
//console.log(packet.p);
if(positions.length != objects.length)
console.warn("Simulation and viewer object count desynced", positions.length, objects.length);
recentServerPositions = positions;
recentServerVelocities = velocities;
recentTickTimestamp = now();
//setPositions(objects, positions);
//console.log(focusPlayer);
}
function newObjects(objList) {
// make new objects
for(var i = 0; i < objList.length; i++) {
var newObj = objList[i];
switch(newObj.type) {
case bodyTypes.ball:
ballMesh = createSphere(newObj);
break;
case bodyTypes.player:
createPlayer(newObj);
break;
case bodyTypes.ground:
createPlane(newObj);
break;
case bodyTypes.popper:
createSphere(newObj);
break;
default:
console.warn("Unhandled object type: " + newObj);
}
}
}
var noMove = new THREE.Vector3(0, 0, 0);
function sendCommands() {
var commands = keyboardCommands.update();
//console.log(commands);
var moveVector = noMove.clone();
var forward = camera.getWorldDirection();
// TODO: Should I try to avoid allocations?
var up = camera.up.clone();
var left = up.clone().cross(forward);
if (commands[keyboardCommands.names.forward]) {
moveVector.add(forward.setLength(1));
}
if (commands[keyboardCommands.names.back]) {
moveVector.add(forward.setLength(-1));
}
if (commands[keyboardCommands.names.left]) {
moveVector.add(left.setLength(1));
}
if (commands[keyboardCommands.names.right]) {
moveVector.add(left.setLength(-1));
}
if (commands[keyboardCommands.names.fly]) {
moveVector.add(up.setLength(1));
}
if (commands[keyboardCommands.names.dive]) {
moveVector.add(up.setLength(-1));
}
if (!moveVector.equals(noMove)) {
socket.emit(types.eventTypes.command, new types.moveCommand(moveVector));
//console.log(moveVector);
}
}
function fixCameraOnStart(packet) {
// I don't yet understand how this thing works.
//controls.target.copy(playerMeshes[focusPlayer].position);
//controls.target = playerMeshes[focusPlayer].position;
//camera.position.copy(playerMeshes[focusPlayer].position);
//var outside = 7;
//if(playerMeshes[focusPlayer].position.y < 0)
// outside = -outside;
//camera.position.setX(outside);
//console.log(camera.position);
//camera.position.setY(camera.position.y + 2);
//camera.position.setZ(camera.position.z + 1);
//playerMeshes[focusPlayer].add(camera);
if(activeControls === autoCameraControls) {
controls.targetNear = playerMeshes[focusPlayer];
controls.targetFar = ballMesh;
}
if(activeControls === pointerLockControls) {
camera.position.setZ(8);
playerMeshes[focusPlayer].add(controls.object);
}
}
viewer.onServerTick = function(packet) {
stats.update();
//console.log(packet);
if(packet.n) {
newObjects(packet.n);
}
if(packet.p) {
updatePositions(packet.p, packet.v);
}
sendCommands();
if(packet.n) {
// TODO: make this less ugly in some "game events/startup" function
fixCameraOnStart();
}
}
viewer.onStartGame = function(playerIdsArray) {
focusPlayer = playerIdsArray.indexOf(thisPlayerId);
console.log('serverStartGame', playerIdsArray, focusPlayer);
};
viewer.onSetPlayerId = function(playerId) {
thisPlayerId = playerId;
console.log("I am", playerId);
};
function scoreToast(line, team) {
humane.log(line, { addnCls: 'team' + team})
}
function gameOverToast(line, team) {
humane.log(line, { timeout: 15000, addnCls: 'wonToast team' + team})
}
function setScore(team, score) {
var el = document.getElementById('score' + team);
el.innerHTML = score;
}
viewer.score = function(data) {
var team = data.teamScored;
var line = types.teamNames[team] + ' scored!';
setScore(team, data.newScore);
scoreToast(line, team)
};
viewer.gameOver = function(data) {
var team = data.teamWon;
setScore(team, data.newScore);
var line = types.teamNames[team] + ' won!';
gameOverToast(line, team)
}
return viewer;
}
function selectRandom(arr) {
// http://stackoverflow.com/questions/4550505/getting-random-value-from-an-array
return arr[Math.floor(Math.random() * arr.length)];
}
function main() {
v = initScene();
window.addEventListener( 'resize', onWindowResize, false );
function animate() {
requestAnimationFrame( animate );
v.onPaint();
};
requestAnimationFrame( animate );
// To run the server locally:
// TODO: make `runLocal` a button in the UI?
var runLocal = socketio === undefined;
if(runLocal) {
socket = {};
socket.callbacks = {};
socket.on = function(eventName, func) {
socket.callbacks[eventName] = func;
};
socket.emit = function(eventName, data) {
return socket.callbacks[eventName](data);
};
} else {
socket = socketio();
}
socket.on(types.eventTypes.setPlayerId, v.onSetPlayerId);
socket.on(types.eventTypes.serverStartGame, v.onStartGame);
socket.on(types.eventTypes.score, v.score);
socket.on(types.eventTypes.gameOver, v.gameOver);
socket.on('error', function(e) {
// TODO: check if this works somehow
humane.error(e);
});
socket.on(types.eventTypes.toast, function(line) {
// NOTE: we don't pass `humane.log`
// as the callback because it ruins `this` and
// you get an undefined queue error.
humane.log(line);
});
socket.on(types.eventTypes.tick, v.onServerTick);
if (runLocal) {
// TODO: Make this more elegant. E.g. remove the command forwarding from the matchmaker.
var playersArray = [{id: 'playerOne', team: 0}, {id:'playerTwo', team: 1}];
thisPlayerId = selectRandom(playersArray).id;
var game = server(socket.emit, playersArray);
socket.on(types.eventTypes.command, function(com) {
com.playerId = thisPlayerId;
game.command(com);
});
} else {
socket.emit(types.eventTypes.clientRequestGame);
}
return v;
}
return main();
});