├── .gitignore ├── demo.js ├── index.js ├── package.json ├── readme.md └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /demo.js: -------------------------------------------------------------------------------- 1 | var server = require('./')() 2 | var port = 8080 3 | server.listen(port) 4 | console.log('Listening on ', port, ' open http://localhost:', port) -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var http = require('http') 2 | var ecstatic = require('ecstatic') 3 | var WebSocketServer = require('ws').Server 4 | var websocket = require('websocket-stream') 5 | var duplexEmitter = require('duplex-emitter') 6 | var path = require('path') 7 | var uuid = require('hat') 8 | var crunch = require('voxel-crunch') 9 | var engine = require('voxel-engine') 10 | var texturePath = require('painterly-textures')(__dirname) 11 | var voxel = require('voxel') 12 | 13 | module.exports = function() { 14 | 15 | // these settings will be used to create an in-memory 16 | // world on the server and will be sent to all 17 | // new clients when they connect 18 | var settings = { 19 | generate: voxel.generator['Valley'], 20 | chunkDistance: 2, 21 | materials: [ 22 | ['grass', 'dirt', 'grass_dirt'], 23 | 'obsidian', 24 | 'brick', 25 | 'grass' 26 | ], 27 | texturePath: texturePath, 28 | worldOrigin: [0, 0, 0], 29 | controls: { discreteFire: true }, 30 | avatarInitialPosition: [2, 20, 2] 31 | } 32 | 33 | var game = engine(settings) 34 | var server = http.createServer(ecstatic(path.join(__dirname, 'www'))) 35 | var wss = new WebSocketServer({server: server}) 36 | var clients = {} 37 | var chunkCache = {} 38 | var usingClientSettings 39 | 40 | // simple version of socket.io's sockets.emit 41 | function broadcast(id, cmd, arg1, arg2, arg3) { 42 | Object.keys(clients).map(function(client) { 43 | if (client === id) return 44 | clients[client].emit(cmd, arg1, arg2, arg3) 45 | }) 46 | } 47 | 48 | function sendUpdate() { 49 | var clientKeys = Object.keys(clients) 50 | if (clientKeys.length === 0) return 51 | var update = {positions:{}, date: +new Date()} 52 | clientKeys.map(function(key) { 53 | var emitter = clients[key] 54 | update.positions[key] = { 55 | position: emitter.player.position, 56 | rotation: { 57 | x: emitter.player.rotation.x, 58 | y: emitter.player.rotation.y 59 | } 60 | } 61 | }) 62 | broadcast(false, 'update', update) 63 | } 64 | 65 | setInterval(sendUpdate, 1000/22) // 45ms 66 | 67 | wss.on('connection', function(ws) { 68 | // turn 'raw' websocket into a stream 69 | var stream = websocket(ws) 70 | 71 | var emitter = duplexEmitter(stream) 72 | 73 | emitter.on('clientSettings', function(clientSettings) { 74 | // Enables a client to reset the settings to enable loading new clientSettings 75 | if (clientSettings != null) { 76 | if (clientSettings.resetSettings != null) { 77 | console.log("resetSettings:true") 78 | usingClientSettings = null 79 | if (game != null) game.destroy() 80 | game = null 81 | chunkCache = {} 82 | } 83 | } 84 | 85 | if (clientSettings != null && usingClientSettings == null) { 86 | usingClientSettings = true 87 | // Use the correct path for textures url 88 | clientSettings.texturePath = texturePath 89 | //deserialise the voxel.generator function. 90 | if (clientSettings.generatorToString != null) { 91 | clientSettings.generate = eval("(" + clientSettings.generatorToString + ")") 92 | } 93 | settings = clientSettings 94 | console.log("Using settings from client to create game.") 95 | game = engine(settings) 96 | } else { 97 | if (usingClientSettings != null) { 98 | console.log("Sending current settings to new client.") 99 | } else { 100 | console.log("Sending default settings to new client.") 101 | } 102 | } 103 | }) 104 | 105 | var id = uuid() 106 | clients[id] = emitter 107 | 108 | emitter.player = { 109 | rotation: new game.THREE.Vector3(), 110 | position: new game.THREE.Vector3() 111 | } 112 | 113 | console.log(id, 'joined') 114 | emitter.emit('id', id) 115 | broadcast(id, 'join', id) 116 | stream.once('end', leave) 117 | stream.once('error', leave) 118 | function leave() { 119 | delete clients[id] 120 | console.log(id, 'left') 121 | broadcast(id, 'leave', id) 122 | } 123 | 124 | emitter.on('message', function(message) { 125 | if (!message.text) return 126 | if (message.text.length > 140) message.text = message.text.substr(0, 140) 127 | if (message.text.length === 0) return 128 | console.log('chat', message) 129 | broadcast(null, 'message', message) 130 | }) 131 | 132 | // give the user the initial game settings 133 | if (settings.generate != null) { 134 | settings.generatorToString = settings.generate.toString() 135 | } 136 | emitter.emit('settings', settings) 137 | 138 | // fires when the user tells us they are 139 | // ready for chunks to be sent 140 | emitter.on('created', function() { 141 | sendInitialChunks(emitter) 142 | // fires when client sends us new input state 143 | emitter.on('state', function(state) { 144 | emitter.player.rotation.x = state.rotation.x 145 | emitter.player.rotation.y = state.rotation.y 146 | var pos = emitter.player.position 147 | var distance = pos.distanceTo(state.position) 148 | if (distance > 20) { 149 | var before = pos.clone() 150 | pos.lerp(state.position, 0.1) 151 | return 152 | } 153 | pos.copy(state.position) 154 | }) 155 | }) 156 | 157 | emitter.on('set', function(pos, val) { 158 | game.setBlock(pos, val) 159 | var chunkPos = game.voxels.chunkAtPosition(pos) 160 | var chunkID = chunkPos.join('|') 161 | if (chunkCache[chunkID]) delete chunkCache[chunkID] 162 | broadcast(null, 'set', pos, val) 163 | }) 164 | 165 | }) 166 | 167 | function sendInitialChunks(emitter) { 168 | Object.keys(game.voxels.chunks).map(function(chunkID) { 169 | var chunk = game.voxels.chunks[chunkID] 170 | var encoded = chunkCache[chunkID] 171 | if (!encoded) { 172 | encoded = crunch.encode(chunk.voxels) 173 | chunkCache[chunkID] = encoded 174 | } 175 | emitter.emit('chunk', encoded, { 176 | position: chunk.position, 177 | dims: chunk.dims, 178 | length: chunk.voxels.length 179 | }) 180 | }) 181 | emitter.emit('noMoreChunks', true) 182 | } 183 | 184 | return server 185 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "voxel-server", 3 | "subdomain": "voxel", 4 | "version": "0.0.1", 5 | "dependencies": { 6 | "minecraft-skin": "0.0.3", 7 | "voxel-walk": "0.0.1", 8 | "voxel-highlight": "0.0.8", 9 | "toolbar": "0.0.5", 10 | "voxel-player": "0.0.2", 11 | "voxel-engine": "~0.16.0", 12 | "voxel-crunch": "0.2.1", 13 | "websocket-stream": "0.0.5", 14 | "ws": "0.4.25", 15 | "ecstatic": "0.3.0", 16 | "hat": "0.0.3", 17 | "duplex-emitter": "0.1.6", 18 | "painterly-textures": "0.0.3", 19 | "voxel": "0.3.1" 20 | }, 21 | "scripts": { 22 | "start": "node demo.js" 23 | }, 24 | "engines": { 25 | "node": "0.8.x" 26 | }, 27 | "devDependencies": { 28 | "websocket-stream": "0.0.5" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # voxel-server 2 | 3 | multiplayer server for [voxel-engine](http://github.com/maxogden/voxel-engine) 4 | 5 | Use with [voxel-client](https://github.com/maxogden/voxel-client) 6 | 7 | If the client sends an object with a settings property, it will use those settings when creating its game instance and will send those instances to other clients that connect. 8 | 9 | If the client settings have the property "resetSettings", the server will switch to those. It deletes any game instance and clears the chunkCache. 10 | 11 | ## Get it running on your machine 12 | 13 | ``` 14 | npm install 15 | ``` 16 | 17 | Run the start script: 18 | 19 | ``` 20 | npm start 21 | ``` 22 | 23 | This gets the server running on port 8080. 24 | 25 | ## explanation 26 | 27 | background research: 28 | 29 | - http://buildnewgames.com/real-time-multiplayer/ 30 | - https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking 31 | - http://www.gabrielgambetta.com/?p=63 (all three parts) 32 | - http://gafferongames.com/networking-for-game-programmers/what-every-programmer-needs-to-know-about-game-networking/ 33 | - http://gafferongames.com/game-physics/networked-physics/ 34 | - http://udn.epicgames.com/Three/NetworkingOverview.html 35 | 36 | ## license 37 | 38 | BSD 39 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var server = require('./')() 2 | var port = 8080 3 | server.listen(port) 4 | var websocketStream = require('websocket-stream') 5 | var WebSocket = require('ws') 6 | var ws = new WebSocket('ws://localhost:8080') 7 | var wsStream = websocketStream(ws) 8 | var duplexEmitter = require('duplex-emitter') 9 | var client = duplexEmitter(wsStream) 10 | 11 | client.on('id', function(id) { 12 | console.log('got id:', id) 13 | }) 14 | 15 | client.on('settings', function(settings) { 16 | console.log('got settings:', settings) 17 | }) 18 | 19 | ws.on('open', function() { 20 | client.emit('created') 21 | }) 22 | 23 | client.on('chunk', function(chunk) { 24 | console.log('got initial chunk') 25 | }) 26 | 27 | client.on('noMoreChunks', function() { 28 | console.log('all done') 29 | process.exit() 30 | }) 31 | --------------------------------------------------------------------------------