Skip to content
Browse files

Version 0.1.0

  • Loading branch information...
1 parent 4d9278e commit c18dd9502dddbdc3a4a4e92647398d7241582a98 @stagas committed May 7, 2011
View
114 README.md
@@ -6,53 +6,64 @@ Installing
----------
npm install maga
-To run the example
+To run the examples
------------------
-If you installed with npm:
+`npm explore maga` or `cd maga` if you cloned:
- npm explore maga
- node examples/simple.js [8080] [localhost]
+ node examples/circles-server.js [8080] [localhost]
+ node examples/cars-server.js [8080] [localhost]
+ node examples/hax-server.js [8080] [localhost]
-If cloned:
-
- cd maga
- node examples/simple.js [8080] [localhost]
-
-
-What it does
+Introduction
------------
-Provides a framework for game development, for syncing state
-across the network using a built-in timestep based
-authority scheme with client prediction.
-
-This means, all
-clients run their own simulation of the entire game while
-compensating for the roundtrip lag by advancing frame steps.
+maga is a framework to assist in game development, syncing physics state
+across the network among multiple clients using a built-in timestep based
+authority scheme with client prediction. It runs in any CommonJS enviroment,
+so you can use it in both node.js and the browser. You get to use
+the same code in your server backend simulation and the browser's simulation.
+
+Every instance is both a sender and a receiver, it runs its own simulation
+of the entire game while compensating for the roundtrip lag by advancing frame steps.
Every client runs to catch the fastest one.
+See the hax example for both server/client usage.
+
How to get started
------------------
You start by creating a new game object like this:
var Maga = require('maga')
+ var game = new Maga.Game()
- var game = new Maga.Game({
- frameTime : 1000 / 45
- , loopTime : 1000 / 135
- , maxFrameTime : 1000 / 45
- , syncTime : 1000 / 15
- })
-
-In this game we can create channels like this:
+In this game we can create rooms like this:
- var channel = game.createChannel()
+ var room = game.createRoom()
+
+The Room
+--------
-And we'll also need a protocol instance:
+`room.watch(object || objectId, function(serialized) { ... })` watches an object for changes and returns a
+serialized string you can then send over the network.
- var protocol = new Maga.Protocol(game, channel)
+`room.parse(serialized, function(state) { ... })` parses a serialized state and returns a state object.
+
+`room.applyState(state)` to apply an incoming state object to the room. Automatically handles client prediction.
+
+`room.on('state', function(state) { ... })` fires whenever a message is parsed with `room.parse()`
+
+`room.loop(fn)` The room's main loop. Calls fn on each iteration. `this` is the room object.
+
+`room.addObject(object)` Add an object to the game.
+
+`room.removeObject(object || objectId)` Remove an object from the game.
+
+`room.stringify(object || objectId)` returns a stringified representation of the object with id.
+This automatically accounts for changes, so if the inputs don't change, it returns nothing.
-You define your objects yourself and inherit from `Maga.Object`.
+Objects
+-------
+You create your objects yourself and inherit from `Maga.Object`.
Here is from the example Circles game source:
var Circle = function() {
@@ -88,22 +99,46 @@ Here is from the example Circles game source:
util.inherits(Circle, Maga.Object)
-Your objects require to have these methods: `.update` `.render` `.destroy`
-The `update` method is called to advances the physics state by 1 frame
+Your objects require to have these methods: `update` `create` `render` and `destroy`
+
+The `update` method advances the physics state by 1 frame.
+
The `render` method is called with an object argument containing
-the render registered properties. You can use them to draw wherever
+the render properties. You can use them to draw wherever
you like, a canvas, the DOM, it's up to you.
+
+The `create` method is used to create the DOM/canvas object.
+
The `destroy` method to remove the object.
-Protocol
---------
+How to handle players
+---------------------
+Luckily, maga comes with a `playerManager` middleware included. You can use it to do simple player
+management for a jump start. Here's an example:
-`protocol.stringify(myId)` returns a stringified representation of the object with myId.
-This automatically accounts for changes, so if the inputs don't change, it returns nothing.
+ // maga
+ var Maga = require('maga')
+ , playerManager = require('middleware/playerManager')
+ , Circles = require('circles')
-`protocol.parse(state_message)` parses a serialized state and returns a state object.
+ // new game
+ var game = new Maga.Game('Circles')
+ , room = game.createRoom()
+ , players = playerManager(room, Circles)
-`protocol.applyState(state)` to apply a state object to the game. Automatically handles client rewind/prediction.
+The `players` object then has these methods:
+
+`players.set(state)` This parses an incoming state, should be used in the `room.parse()` callback just before `room.applyState()`
+
+`state = players.get()` Gets current state
+
+`player = players.create(id)` Create a new player using our object constructor
+
+`players.remove(object || id)` Remove a player
+
+`me = players.createMyself(id)` Create our character
+
+`players.forEach(function(player, id) { ... })` Iterate players in the room
How to require() in the browser?
--------------------------------
@@ -118,7 +153,6 @@ and then you can do this in your express server:
app.expose({ inherits: util.inherits }, 'util')
app.exposeModule(__dirname + '/../maga', 'maga')
app.exposeModule(__dirname + '/circles', 'circles')
-
app.get('/exposed.js', function(req, res) {
res.setHeader('Content-Type', 'application/javascript')
res.send(app.exposed())
View
122 examples/cars-server.js
@@ -1,6 +1,8 @@
-//
-// Simple example server
-//
+/*
+ * Cars game server
+ */
+
+require.paths.unshift(__dirname, __dirname + '/../')
var util = require('util')
, express = require('express')
@@ -12,71 +14,99 @@ config.port = +(process.env.PORT || process.env.POLLA_PORT || process.argv[2] ||
config.host = process.env.HOST || process.env.POLLA_HOST || process.argv[3] || config.host || 'localhost'
config.address = 'http://' + config.host + (process.env.PORT || process.env.POLLA_PORT || config.port == 80 ? '' : ':' + config.port)
-function log() {
- var args = [].slice.call(arguments)
- var d = new Date()
- args.unshift(
- d.toUTCString().split(' ').splice(1,2).join(' ')
- , d.toTimeString().split(' ').splice(0,1).join(' ')
- , '-'
- , config.address
- , '-'
- )
- console.log.apply(this, args)
-}
-var initDate = new Date().toUTCString()
-
+// http server
var app = express.createServer()
+// middleware
+app.use(function(req, res, next) {
+ log(req.url, req.connection.remoteAddress)
+ next()
+})
+app.use(function(req, res, next) {
+ if (req.url === '/') req.url = '/cars.html'
+ next()
+})
app.use(app.router)
app.use(express.static(__dirname))
+// expose libs to client
app.exposeRequire()
app.expose(config, 'config')
app.expose({ inherits: util.inherits }, 'util')
-app.exposeModule(__dirname + '/../maga', 'maga')
-app.exposeModule(__dirname + '/keys', 'keys')
-app.exposeModule(__dirname + '/cars', 'cars')
-
+app.exposeModule('lib/events', 'events')
+app.exposeModule('maga', 'maga')
+app.exposeModule('middleware/playerManager', 'middleware/playerManager')
+app.exposeModule('lib/keys', 'lib/keys')
+app.exposeModule('lib/helpers', 'lib/helpers')
+app.exposeModule('cars', 'cars')
app.get('/exposed.js', function(req, res) {
- res.setHeader('Content-Type', 'application/javascript')
- res.setHeader('Last-Modified', initDate)
- if (req.headers['if-modified-since'] === initDate) {
- return res.send(304)
- }
res.send(app.exposed())
})
-var cnt = 0
-var queue = []
-var flush = function() {
- var q
- while (q = queue.shift()) {
- log('flush', ++cnt)
- q.client.broadcast(q.message)
- }
-}
+// listen http server
+app.listen(config.port, config.host, function() {
+ log('HTTP Server listening')
+})
+
+// maga
+var Maga = require('maga')
+ , playerManager = require('middleware/playerManager')
+ , Cars = require('cars')
+
+// new game
+var game = new Maga.Game('Cars')
+ , room = game.createRoom()
+ , players = playerManager(room, Cars)
+
+// main loop
+room.loop()
+// socket.io server
var socket = io.listen(app)
+
socket.on('connection', function(client) {
var playerId = parseInt(client.sessionId, 10).toString(32)
log('***** Joined:', playerId)
- config['test latency'] && flush()
+
+ // send other players state to our newly joined client
+ players.forEach(function(player, id) {
+ client.send(room.stringify(player, true))
+ })
+
+ // add player to room
+ var player = players.create(playerId)
+ room.addObject(player)
+
+ // parse incoming client action
client.on('message', function(message) {
- if (config['test latency']) {
- queue.push({ client: client, message: message })
- setTimeout(function() {
- flush()
- }, Math.random() * config['test latency'] | 0)
- } else client.broadcast(message)
+ room.parse(message, function(state) {
+ players.set(state)
+ room.applyState(state)
+ })
+
+ // broadcast to other clients
+ client.broadcast(message)
})
+
+ // player disconnected
client.on('disconnect', function() {
var playerId = parseInt(this.sessionId, 10).toString(32)
- socket.broadcast(JSON.stringify({ 0: { disconnectId: playerId } }))
log('***** Left:', playerId)
+ players.remove(playerId)
+ socket.broadcast(JSON.stringify({ 0: { disconnectId: playerId } }))
})
})
-app.listen(config.port, config.host, function() {
- log('HTTP Server listening')
-})
+// utilities
+function log() {
+ var args = [].slice.call(arguments)
+ var d = new Date()
+ args.unshift(
+ d.toUTCString().split(' ').splice(1,2).join(' ')
+ , d.toTimeString().split(' ').splice(0,1).join(' ')
+ , '-'
+ , config.address
+ , '-'
+ )
+ console.log.apply(this, args)
+}
View
94 examples/cars.html
@@ -10,69 +10,48 @@
$(function() {
-var util = require('util')
- , Maga = require('maga')
- , Keys = require('keys')
- , Cars = require('cars')
- , config = require('config')
+var config = require('config')
+ , socket = new io.Socket(config.host)
-var keys = new Keys()
-var socket = new io.Socket(config.host)
+// maga
+var Maga = require('maga')
+ , playerManager = require('middleware/playerManager')
+ , keys = new(require('lib/keys'))
+ , Cars = require('cars')
-var game = new Maga.Game({
- frameTime : 1000 / 45
- , loopTime : 1000 / 135
- , maxFrameTime : 1000 / 46
- , syncTime : 1000 / 15
- })
- , channel = game.createChannel()
- , protocol = new Maga.Protocol(game, channel)
- , myId
+// new game
+var game = new Maga.Game('Cars')
+ , room = game.createRoom()
+ , players = playerManager(room, Cars)
, me
- , players = {}
+// socket.io
socket.on('connect', function() {
- myId = parseInt(socket.transport.sessionid, 10).toString(32)
- me = new Cars.Player(myId).create()
-
- players[myId] = me
- channel.addObject(me)
-
- /*
- $('body').click(function() {
- game.set('frameTime', Math.max(game.get('frameTime') + (Math.random() * 300 - 260), 1000 / 30))
+ // add our own player to the game
+ var me = players.createMyself(parseInt(socket.transport.sessionid, 10).toString(32))
+ room.addObject(me)
+ room.watch(me, function(str) {
+ socket.send(str)
})
- */
-
- channel.loop()
- var str
- setInterval(function() {
+ // main loop
+ room.loop(function() {
+ // keyboard input
me.processKeys(keys)
- str = protocol.stringify(myId)
- if (str && str.length) socket.send(str)
- }, game.syncTime)
+ $('#console').text(this.state.frame)
+ })
})
+
socket.on('message', function(message) {
- var state = protocol.parse(message)
- for (var frame in state) {
- for (var id in state[frame]) {
- if (id == myId) {
- delete state[frame][id]
- continue
- } else if (id == 'disconnectId') {
- console.log('DISCONNECTED', state[frame].disconnectId)
- channel.removeObject(state[frame].disconnectId)
- } else if (!players.hasOwnProperty(id)) {
- console.log('CREATING PLAYER ' + id)
- players[id] = new Cars.Player(id).create()
- channel.addObject(players[id])
- }
- }
- }
- protocol.applyState(myId, state)
+ room.parse(message, function(state) {
+ players.set(state)
+ room.applyState(state)
+ })
+})
+
+socket.on('disconnect', function() {
+ players.remove(me)
})
-socket.on('disconnect', function() {})
socket.connect()
@@ -94,10 +73,21 @@
margin-top:-30px;
border-radius:2px;
}
+#console {
+position:fixed;
+top:0;
+left:0;
+z-index:10000;
+color:#BBB;
+font-family:Courier;
+width:300px;
+height:200px;
+}
</style>
</head>
<body>
+<div id="console"></div>
<a href="http://github.com/stagas/maga" target="_blank"><img style="position: absolute; top: 0; right: 0; border: 0; z-index:10000;" src="https://d3nwyuy0nl342s.cloudfront.net/img/4c7dc970b89fd04b81c8e221ba88ff99a06c6b61/687474703a2f2f73332e616d617a6f6e6177732e636f6d2f6769746875622f726962626f6e732f666f726b6d655f72696768745f77686974655f6666666666662e706e67" alt="Fork me on GitHub"></a>
</body>
</html>
View
238 examples/cars.js
@@ -1,127 +1,111 @@
-//
-// Cars game
-//
-
-Math.PIHC = Math.PI / 180
-
-Math.cosa = function(a) {
- return Math.round(Math.cos(a * Math.PIHC) * 10000) / 10000
-}
-
-Math.sina = function(a) {
- return Math.round(Math.sin(a * Math.PIHC) * 10000) / 10000
-}
-
-Math.sgn = function(a) {
- if (a<0) return -1
- else if (a>0) return 1
- else return 0
-}
-
-var util = require('util')
- , Keys = require('keys')
- , Maga = require('maga')
-
-var Car = function() {
- Maga.Object.apply(this, arguments)
-
- this.register({
- // Values used to render (draw) object
- render: {
- x: 100
- , y: 100
- , a: 0
- }
-
- // Input values that determine behavior
- , input: {
- steer: 0
- , torque: 0
- , brakes: 0
- }
-
- // Values that change
- , dynamic: {
- keys: 0
- , v: 0
- , vx: 0
- , vy: 0
- }
-
- // Values that don't change
- , static: {
- f: 0.985
- , acc: 1.044
- , maxVelocity: 14
- , steerSpeed: 4
- }
- })
-}
-
-util.inherits(Car, Maga.Object)
-
-Car.prototype.update = function() {
- this.a += this.steer * this.steerSpeed
- if (this.torque) {
- this.v = Math.min(Math.max(1, this.v * ((5 / (1 + this.v)) * this.acc), this.maxVelocity))
- // calculate new xy velocities
- this.vx += (this.v * Math.cosa(this.a)) / 50
- this.vy += (this.v * Math.sina(this.a)) / 50
- } else {
- // calculate new xy velocities
- this.vx += (this.v * Math.cosa(this.a)) / 200
- this.vy += (this.v * Math.sina(this.a)) / 200
- }
- this.x += this.vx
- this.y += this.vy
- if (this.x < 30) this.x = 30
- if (this.x > 1200) this.x = 1200
- if (this.y < 50) this.y = 50
- if (this.y > 600) this.y = 600
- this.vx *= this.f
- this.vy *= this.f
- this.v *= this.f
- return this
-}
-
-Car.prototype.processKeys = function(keys) {
- this.keys = keys
- if (keys.up) this.torque = 1, this.brakes = 0
- if (keys.right) this.steer = 1
- if (keys.left) this.steer = -1
- if (keys.down) this.torque = 0, this.brakes = 1
- if (!keys.right && !keys.left) this.steer = 0
- if (!keys.up) this.torque = 0
- return this
-}
-
-Car.prototype.create = function() {
- var val = parseInt(this.id, 32), r, g, b
- r = parseInt(val.toString().substr(5, 3), 10)
- while (r > 255) { r = Math.floor(r / 2) }
- g = parseInt(val.toString().substr(9, 3), 10)
- while (g > 255) { g = Math.floor(g / 2) }
- b = parseInt(val.toString().substr(11, 3), 10)
- while (b > 255) { b = Math.floor(b / 2) }
-
- this.object = $('<div class="car" style="background-color:rgb(' + [r,g,b] + ') !important;" id="'+ this.id +'"></div>')
- this.object.appendTo('body')
-
- return this
-}
-
-Car.prototype.render = function(state) {
- this.object.css({ left: state.x, top: state.y })
- this.object.css('transform', 'rotate(' + state.a + 'deg)')
- return this
-}
-
-Car.prototype.destroy = function() {
- this.object.remove()
-}
-
-// The player!
-var Player = exports.Player = function() {
- Car.apply(this, arguments)
-}
-util.inherits(Player, Car)
+//
+// Cars game
+//
+
+var util = require('util')
+ , help = require('lib/helpers')
+ , Maga = require('maga')
+
+var Car = function() {
+ Maga.Object.apply(this, arguments)
+
+ this.register({
+ // Values used to render (draw) object
+ render: {
+ x: 100
+ , y: 100
+ , a: 0
+ }
+
+ // Input values that determine behavior
+ , input: {
+ steer: 0
+ , torque: 0
+ , brakes: 0
+ }
+
+ // Values that change
+ , dynamic: {
+ keys: 0
+ , v: 0
+ , vx: 0
+ , vy: 0
+ }
+
+ // Values that don't change
+ , static: {
+ f: 0.985
+ , acc: 1.044
+ , maxVelocity: 14
+ , steerSpeed: 4
+ }
+ })
+}
+
+util.inherits(Car, Maga.Object)
+
+Car.prototype.update = function() {
+ this.a += this.steer * this.steerSpeed
+ if (this.torque) {
+ this.v = Math.min(Math.max(1, this.v * ((5 / (1 + this.v)) * this.acc), this.maxVelocity))
+ // calculate new xy velocities
+ this.vx += (this.v * help.cosa(this.a)) / 50
+ this.vy += (this.v * help.sina(this.a)) / 50
+ } else {
+ // calculate new xy velocities
+ this.vx += (this.v * help.cosa(this.a)) / 200
+ this.vy += (this.v * help.sina(this.a)) / 200
+ }
+ this.x += this.vx
+ this.y += this.vy
+ if (this.x < 30) this.x = 30
+ if (this.x > 1200) this.x = 1200
+ if (this.y < 50) this.y = 50
+ if (this.y > 600) this.y = 600
+ this.vx *= this.f
+ this.vy *= this.f
+ this.v *= this.f
+ return this
+}
+
+Car.prototype.processKeys = function(keys) {
+ this.keys = keys
+ if (keys.up) this.torque = 1, this.brakes = 0
+ if (keys.right) this.steer = 1
+ if (keys.left) this.steer = -1
+ if (keys.down) this.torque = 0, this.brakes = 1
+ if (!keys.right && !keys.left) this.steer = 0
+ if (!keys.up) this.torque = 0
+ return this
+}
+
+Car.prototype.create = function() {
+ var val = parseInt(this.id, 32), r, g, b
+ r = parseInt(val.toString().substr(5, 3), 10)
+ while (r > 255) { r = Math.floor(r / 2) }
+ g = parseInt(val.toString().substr(9, 3), 10)
+ while (g > 255) { g = Math.floor(g / 2) }
+ b = parseInt(val.toString().substr(11, 3), 10)
+ while (b > 255) { b = Math.floor(b / 2) }
+
+ this.object = $('<div class="car" style="background-color:rgb(' + [r,g,b] + ') !important;" id="'+ this.id +'"></div>')
+ this.object.appendTo('body')
+
+ return this
+}
+
+Car.prototype.render = function(state) {
+ this.object.css({ left: state.x, top: state.y })
+ this.object.css('transform', 'rotate(' + state.a + 'deg)')
+ return this
+}
+
+Car.prototype.destroy = function() {
+ this.object.remove()
+}
+
+// The player!
+var Player = exports.Player = function() {
+ Car.apply(this, arguments)
+}
+util.inherits(Player, Car)
View
118 examples/simple.js → examples/circles-server.js
@@ -1,4 +1,8 @@
-// simple example server
+/*
+ * Circles game server
+ */
+
+require.paths.unshift(__dirname, __dirname + '/../')
var util = require('util')
, express = require('express')
@@ -10,72 +14,98 @@ config.port = +(process.env.PORT || process.env.POLLA_PORT || process.argv[2] ||
config.host = process.env.HOST || process.env.POLLA_HOST || process.argv[3] || config.host || 'localhost'
config.address = 'http://' + config.host + (process.env.PORT || process.env.POLLA_PORT || config.port == 80 ? '' : ':' + config.port)
-function log() {
- var args = [].slice.call(arguments)
- var d = new Date()
- args.unshift(
- d.toUTCString().split(' ').splice(1,2).join(' ')
- , d.toTimeString().split(' ').splice(0,1).join(' ')
- , '-'
- , config.address
- , '-'
- )
- console.log.apply(this, args)
-}
-var initDate = new Date().toUTCString()
-
+// http server
var app = express.createServer()
+// middleware
+app.use(function(req, res, next) {
+ log(req.url, req.connection.remoteAddress)
+ next()
+})
+app.use(function(req, res, next) {
+ if (req.url === '/') req.url = '/circles.html'
+ next()
+})
app.use(app.router)
app.use(express.static(__dirname))
+// expose libs to client
app.exposeRequire()
app.expose(config, 'config')
app.expose({ inherits: util.inherits }, 'util')
-app.exposeModule(__dirname + '/../maga', 'maga')
-app.exposeModule(__dirname + '/circles', 'circles')
-
+app.exposeModule('lib/events', 'events')
+app.exposeModule('maga', 'maga')
+app.exposeModule('middleware/playerManager', 'middleware/playerManager')
+app.exposeModule('lib/helpers', 'lib/helpers')
+app.exposeModule('circles', 'circles')
app.get('/exposed.js', function(req, res) {
- res.setHeader('Content-Type', 'application/javascript')
- res.setHeader('Last-Modified', initDate)
- if (req.headers['if-modified-since'] === initDate) {
- return res.send(304)
- }
res.send(app.exposed())
})
-var cnt = 0
-var queue = []
-var flush = function() {
- var q
- while (q = queue.shift()) {
- log('flush', ++cnt)
- q.client.broadcast(q.message)
- }
-}
+// listen http server
+app.listen(config.port, config.host, function() {
+ log('HTTP Server listening')
+})
+
+// maga
+var Maga = require('maga')
+ , playerManager = require('middleware/playerManager')
+ , Circles = require('circles')
+
+// new game
+var game = new Maga.Game('Circles')
+ , room = game.createRoom()
+ , players = playerManager(room, Circles)
+
+// main loop
+room.loop()
+// socket.io server
var socket = io.listen(app)
socket.on('connection', function(client) {
var playerId = parseInt(client.sessionId, 10).toString(32)
log('***** Joined:', playerId)
- config['test latency'] && flush()
+
+ // send other players state to our newly joined client
+ players.forEach(function(player, id) {
+ client.send(room.stringify(player, true))
+ })
+
+ // add player to room
+ var player = players.create(playerId)
+ room.addObject(player)
+
+ // parse incoming client action
client.on('message', function(message) {
- if (config['test latency']) {
- queue.push({ client: client, message: message })
- setTimeout(function() {
- flush()
- }, Math.random() * config['test latency'] | 0)
- } else client.broadcast(message)
+ room.parse(message, function(state) {
+ players.set(state)
+ room.applyState(state)
+ })
+
+ // broadcast to other clients
+ client.broadcast(message)
})
+
+ // player disconnected
client.on('disconnect', function() {
var playerId = parseInt(this.sessionId, 10).toString(32)
- socket.broadcast(JSON.stringify({ 0: { disconnectId: playerId } }))
log('***** Left:', playerId)
+ players.remove(playerId)
+ socket.broadcast(JSON.stringify({ 0: { disconnectId: playerId } }))
})
})
-app.listen(config.port, config.host, function() {
- log('HTTP Server listening')
-})
-
+// utilities
+function log() {
+ var args = [].slice.call(arguments)
+ var d = new Date()
+ args.unshift(
+ d.toUTCString().split(' ').splice(1,2).join(' ')
+ , d.toTimeString().split(' ').splice(0,1).join(' ')
+ , '-'
+ , config.address
+ , '-'
+ )
+ console.log.apply(this, args)
+}
View
90 examples/circles.html
@@ -0,0 +1,90 @@
+<!doctype html>
+<html>
+<head>
+<title>maga</title>
+<script src="/exposed.js"></script>
+<script src="/jquery.js"></script>
+<script src="/socket.io/socket.io.js"></script>
+<script>
+
+$(function() {
+
+var config = require('config')
+ , socket = new io.Socket(config.host)
+
+// maga
+var Maga = require('maga')
+ , playerManager = require('middleware/playerManager')
+ , Circles = require('circles')
+
+// new game
+var game = new Maga.Game('Circles')
+ , room = game.createRoom()
+ , players = playerManager(room, Circles)
+ , me
+
+// socket.io
+socket.on('connect', function() {
+ // add our own player to the game
+ var me = players.createMyself(parseInt(socket.transport.sessionid, 10).toString(32))
+ room.addObject(me)
+ room.watch(me, function(str) {
+ socket.send(str)
+ })
+
+ // send mouse input to our player
+ $('body').mousemove(function(ev) {
+ me.move(ev.pageX, ev.pageY)
+ })
+
+ // main loop
+ room.loop(function() {
+ $('#console').text(this.state.frame)
+ })
+})
+
+socket.on('message', function(message) {
+ room.parse(message, function(state) {
+ players.set(state)
+ room.applyState(state)
+ })
+})
+
+socket.on('disconnect', function() {
+ players.remove(me)
+})
+
+socket.connect()
+
+})
+</script>
+
+<style>
+html, body {
+width:100%;
+height:100%;
+overflow:hidden;
+background:#070707;
+}
+.circle {
+position:absolute;
+border-radius:1000px;
+}
+#console {
+position:fixed;
+top:0;
+left:0;
+z-index:10000;
+color:#BBB;
+font-family:Courier;
+width:300px;
+height:200px;
+}
+</style>
+
+</head>
+<body>
+<div id="console"></div>
+<a href="http://github.com/stagas/maga" target="_blank"><img style="position: absolute; top: 0; right: 0; border: 0; z-index:10000;" src="https://d3nwyuy0nl342s.cloudfront.net/img/4c7dc970b89fd04b81c8e221ba88ff99a06c6b61/687474703a2f2f73332e616d617a6f6e6177732e636f6d2f6769746875622f726962626f6e732f666f726b6d655f72696768745f77686974655f6666666666662e706e67" alt="Fork me on GitHub"></a>
+</body>
+</html>
View
61 examples/circles.js
@@ -1,29 +1,14 @@
-//
-// Simple circles game
-//
-
-Math.PIHC = Math.PI / 180
-
-Math.cosa = function(a) {
- return Math.round(Math.cos(a * Math.PIHC) * 10000) / 10000
-}
-
-Math.sina = function(a) {
- return Math.round(Math.sin(a * Math.PIHC) * 10000) / 10000
-}
-
-Math.sgn = function(a) {
- if (a<0) return -1
- else if (a>0) return 1
- else return 0
-}
+/*
+ * Simple circles game
+ */
var util = require('util')
+ , help = require('lib/helpers')
, Maga = require('maga')
var Circle = function() {
Maga.Object.apply(this, arguments)
-
+
var val = parseInt(this.id, 32), size
size = parseInt(val.toString().substr(0, 2), 10)
@@ -59,9 +44,6 @@ var Circle = function() {
util.inherits(Circle, Maga.Object)
-function randOrd(){
-return (Math.round(Math.random())-0.5); }
-
Circle.prototype.update = function() {
this.vx += ((this.tx - this.ox) / 135)
this.vy += ((this.ty - this.oy) / 135)
@@ -75,30 +57,21 @@ Circle.prototype.update = function() {
var self = this
var dist, minDist, dx, dy, angle, sx, sy
, tx, ty
- Object.keys(this.channel.objects).sort(randOrd).forEach(function(id) {
-
+ Object.keys(this.room.objects).sort(help.randOrd).forEach(function(id) {
if (id == self.id) return
- var obj = self.channel.objects[id]
+ var obj = self.room.objects[id]
if (!obj.x || !obj.y) return
- dx = obj.x - self.x
- dy = obj.y - self.y
- dist = Math.sqrt(dx*dx + dy*dy)
- minDist = self.width / 2 + obj.width / 2
- if (dist < minDist) {
- angle = Math.atan2(dy, dx)
- tx = self.x + (Math.cos(angle) * minDist)
- ty = self.y + (Math.sin(angle) * minDist)
- sx = tx - obj.x
- sy = ty - obj.y
-
- self.x -= sx
- self.y -= sy
- self.vx -= sx / 3
- self.vy -= sy / 3
- obj.vx += sx / 3
- obj.vy += sy / 3
+ var c = help.circleToCircle(self, obj)
+
+ if (c.collided) {
+ self.x -= c.sx
+ self.y -= c.sy
+ self.vx -= c.sx / 12
+ self.vy -= c.sy / 12
+ obj.vx += c.sx / 12
+ obj.vy += c.sy / 12
}
})
this.ox = this.x
@@ -140,3 +113,5 @@ var Player = exports.Player = function() {
Circle.apply(this, arguments)
}
util.inherits(Player, Circle)
+
+// helpers
View
7 examples/config.json
@@ -1,5 +1,4 @@
-{
- "host": "localhost"
-, "port": "8080"
-, "test latency": 0
+{ "host": "localhost"
+, "port": "8080"
+, "test latency": 0
}
View
133 examples/hax-server.js
@@ -0,0 +1,133 @@
+/*
+ * Cars game server
+ */
+
+require.paths.unshift(__dirname, __dirname + '/../')
+
+var util = require('util')
+ , express = require('express')
+ , expose = require('express-expose')
+ , io = require('socket.io')
+ , config = require('confu')(__dirname, 'config.json')
+
+config.port = +(process.env.PORT || process.env.POLLA_PORT || process.argv[2] || config.port || 8080)
+config.host = process.env.HOST || process.env.POLLA_HOST || process.argv[3] || config.host || 'localhost'
+config.address = 'http://' + config.host + (process.env.PORT || process.env.POLLA_PORT || config.port == 80 ? '' : ':' + config.port)
+
+// http server
+var app = express.createServer()
+
+// middleware
+app.use(function(req, res, next) {
+ log(req.url, req.connection.remoteAddress)
+ next()
+})
+app.use(function(req, res, next) {
+ if (req.url === '/') req.url = '/hax.html'
+ next()
+})
+app.use(app.router)
+app.use(express.static(__dirname))
+
+// expose libs to client
+app.exposeRequire()
+app.expose(config, 'config')
+app.expose({ inherits: util.inherits }, 'util')
+app.exposeModule('lib/events', 'events')
+app.exposeModule('maga', 'maga')
+app.exposeModule('middleware/playerManager', 'middleware/playerManager')
+app.exposeModule('lib/keys', 'lib/keys')
+app.exposeModule('lib/helpers', 'lib/helpers')
+app.exposeModule('hax', 'hax')
+app.get('/exposed.js', function(req, res) {
+ res.send(app.exposed())
+})
+
+// listen http server
+app.listen(config.port, config.host, function() {
+ log('HTTP Server listening')
+})
+
+// maga
+var Maga = require('maga')
+ , playerManager = require('middleware/playerManager')
+ , help = require('lib/helpers')
+ , Hax = require('hax')
+
+// new game
+var game = new Maga.Game('Hax', { syncTime: 1000 / 1, lag: 50 }) // todo: determine lag
+ , room = game.createRoom()
+ , players = playerManager(room, Hax, { ignore: 'ball' })
+
+var ball = new Hax.Ball()
+room.addObject(ball)
+
+// todo: this is common, should be an internal method
+var statePrevious = {}
+setInterval(function() {
+ var obj = {}
+ var diff = room.state.compare(room.state.current, statePrevious)
+ if (diff.changes) {
+ obj[room.state.frame] = diff.diff
+ socket.broadcast(JSON.stringify(obj))
+ }
+ statePrevious = room.state.current || {}
+}, 1000)
+
+// main loop
+room.loop(function() {
+ //
+})
+
+// socket.io server
+var socket = io.listen(app)
+
+socket.on('connection', function(client) {
+ var playerId = parseInt(client.sessionId, 10).toString(32)
+ log('***** Joined:', playerId)
+
+ // send ball
+ client.send(room.stringify(ball, true))
+
+ // send other players state to our newly joined client
+ players.forEach(function(player, id) {
+ client.send(room.stringify(player, true))
+ })
+
+ // add player to room
+ var player = players.create(playerId)
+ room.addObject(player)
+
+ // parse incoming client action
+ client.on('message', function(message) {
+ room.parse(message, function(state) {
+ players.set(state)
+ room.applyState(state)
+ })
+
+ // broadcast to other clients
+ client.broadcast(message)
+ })
+
+ // player disconnected
+ client.on('disconnect', function() {
+ var playerId = parseInt(this.sessionId, 10).toString(32)
+ log('***** Left:', playerId)
+ players.remove(playerId)
+ socket.broadcast(JSON.stringify({ 0: { disconnectId: playerId } }))
+ })
+})
+
+// utilities
+function log() {
+ var args = [].slice.call(arguments)
+ var d = new Date()
+ args.unshift(
+ d.toUTCString().split(' ').splice(1,2).join(' ')
+ , d.toTimeString().split(' ').splice(0,1).join(' ')
+ , '-'
+ , config.address
+ , '-'
+ )
+ console.log.apply(this, args)
+}
View
119 examples/hax.html
@@ -0,0 +1,119 @@
+<!doctype html>
+<html>
+<head>
+<title>maga</title>
+<script src="/exposed.js"></script>
+<script src="/jquery.js"></script>
+<script src="/jquery.transform.js"></script>
+<script src="/socket.io/socket.io.js"></script>
+<script>
+
+$(function() {
+
+var config = require('config')
+ , socket = new io.Socket(config.host)
+
+// maga
+var Maga = require('maga')
+ , playerManager = require('middleware/playerManager')
+ , keys = new(require('lib/keys'))
+ , help = require('lib/helpers')
+ , Hax = require('hax')
+
+// new game
+var game = new Maga.Game('Hax', { lag: 110 })
+ , room = game.createRoom()
+ , players = playerManager(room, Hax, { ignore: [ 'ball' ] })
+ , me, ball
+
+// socket.io
+socket.on('connect', function() {
+ // add our own player to the game
+ var me = players.createMyself(parseInt(socket.transport.sessionid, 10).toString(32))
+ room.addObject(me)
+ room.watch(me, function(str) {
+ socket.send(str)
+ })
+
+ // main loop
+ room.loop(function() {
+ $('#console').text(this.state.frame)
+
+ // keyboard input
+ me.processKeys(keys)
+
+ if (ball) {
+ var collided = false
+ var c = help.circleToCircle(me, ball)
+ ball.collided = c.collided
+ if (ball.collided) {
+ room.watch(ball, function(str) {
+ socket.send(str)
+ })
+ } else {
+ room.unwatch(ball)
+ }
+ }
+ })
+})
+
+socket.on('message', function(message) {
+ room.parse(message, function(state) {
+ for (var frame in state) {
+ if (state[frame].ball) {
+ if (!ball) {
+ ball = new Hax.Ball()
+ ball.create()
+ room.addObject(ball)
+ }
+ }
+ }
+ players.set(state)
+ room.applyState(state)
+ })
+})
+
+socket.on('disconnect', function() {
+ players.remove(me)
+})
+
+socket.connect()
+
+})
+</script>
+
+<style>
+html, body {
+width:100%;
+height:100%;
+overflow:hidden;
+background:#070707;
+}
+.circle {
+position:absolute;
+border-radius:1000px;
+}
+.me {
+box-shadow: 0px 0px 10px 0px #999;
+}
+.highlight {
+box-shadow: 0px 0px 2px 2px #69F;
+}
+#console {
+position:fixed;
+top:0;
+left:0;
+z-index:10000;
+color:#BBB;
+font-family:Courier;
+width:300px;
+height:200px;
+}
+</style>
+
+</head>
+<body>
+<div id="console"></div>
+<a href="http://github.com/stagas/maga" target="_blank"><img style="position: absolute; top: 0; right: 0; border: 0; z-index:10000;" src="https://d3nwyuy0nl342s.cloudfront.net/img/4c7dc970b89fd04b81c8e221ba88ff99a06c6b61/687474703a2f2f73332e616d617a6f6e6177732e636f6d2f6769746875622f726962626f6e732f666f726b6d655f72696768745f77686974655f6666666666662e706e67" alt="Fork me on GitHub"></a>
+</body>
+</html>
View
172 examples/hax.js
@@ -0,0 +1,172 @@
+//
+// Haxball game
+//
+
+var util = require('util')
+ , help = require('lib/helpers')
+ , Maga = require('maga')
+
+var Circle = function() {
+ Maga.Object.apply(this, arguments)
+
+ var val = parseInt(this.id, 32), size
+ size = 30
+
+ this.register({
+ // Values used to render (draw) object
+ render:
+ { x: Math.random() * 800 | 0
+ , y: Math.random() * 400 | 0
+ }
+
+ // Input values that determine behavior
+ , input:
+ { k: 0
+ , shot: 0
+ , shoot: false
+ , name: val
+ }
+
+ // Values that change
+ , dynamic:
+ { keys: {}
+ , v: 0
+ , vx: 0
+ , vy: 0
+ , a: 0
+ }
+
+ // Values that don't change
+ , static:
+ { f: 0.971
+ , acc: 0.17
+ , bounce: 0.35
+ , maxVelocity: 5
+ , steer: 14
+ , width: size
+ , height: size
+ , color: null
+ }
+ })
+}
+
+util.inherits(Circle, Maga.Object)
+
+Circle.prototype.update = function() {
+ var hw = this.width / 2 | 0
+ , hh = this.height / 2 | 0
+
+ if (this.keys.up) this.vy -= this.acc
+ if (this.keys.down) this.vy += this.acc
+ if (this.keys.left) this.vx -= this.acc
+ if (this.keys.right) this.vx += this.acc
+ if (this.vx > this.maxVelocity) this.vx = this.maxVelocity
+ else if (this.vx < -this.maxVelocity) this.vx = -this.maxVelocity
+ if (this.vy > this.maxVelocity) this.vy = this.maxVelocity
+ else if (this.vy < -this.maxVelocity) this.vy = -this.maxVelocity
+
+ this.x += this.vx
+ this.y += this.vy
+ if (this.x < hw) this.x = hw, this.vx = -this.vx * this.bounce
+ if (this.x > 800 - hw) this.x = 800 - hw, this.vx = -this.vx * this.bounce
+ if (this.y < hh) this.y = hh, this.vy = -this.vy * this.bounce
+ if (this.y > 400 - hh) this.y = 400 - hh, this.vy = -this.vy * this.bounce
+ this.vx *= this.f
+ this.vy *= this.f
+
+ var self = this
+ var dist, minDist, dx, dy, angle, sx, sy
+ , tx, ty
+
+ Object.keys(this.room.objects).sort(help.randOrd).forEach(function(id) {
+ if (id == self.id) return
+
+ var obj = self.room.objects[id]
+ if (!obj.x || !obj.y) return
+
+ var c = help.circleToCircle(self, obj)
+
+ if (c.collided) {
+ self.x -= c.sx
+ self.y -= c.sy
+ self.vx -= c.sx / 12
+ self.vy -= c.sy / 12
+ obj.vx += c.sx / 12
+ obj.vy += c.sy / 12
+
+ if (self.id === 'ball' && obj.shoot) {
+ self.vx -= c.sx * 50
+ self.vy -= c.sy * 50
+ } else if (obj.id === 'ball' && self.shoot) {
+ obj.vx += c.sx * 50
+ obj.vy += c.sy * 50
+ }
+
+ }
+ })
+ this.ox = this.x
+ this.oy = this.y
+ return this
+}
+
+Circle.prototype.processKeys = function(keys) {
+ this.keys = keys
+ this.k = keys.pressed
+ this.shoot = keys.shoot ? true : false
+ return this
+}
+
+Circle.prototype.create = function() {
+ var val = parseInt(this.id, 32), r, g, b
+ if (!this.color) {
+ if (val % 2 === 0) {
+ r = 0
+ g = 0
+ b = 255
+ } else {
+ r = 255
+ g = 0
+ b = 0
+ }
+ } else r = this.color[0], g = this.color[1], b = this.color[2]
+
+ this.object = $('<div class="circle" style="background-color:rgb(' + [r,g,b] + ') !important; width:' + this.width +'px; height:' + this.height + 'px; margin-left: -'+Math.floor(this.width/2)+'px; margin-top: -'+Math.floor(this.height/2)+'px;" id="'+ this.id +'"></div>')
+ this.object.appendTo('body')
+ return this
+}
+
+Circle.prototype.render = function(state) {
+ this.object.css({ left: state.x, top: state.y })
+ if (this.oldShoot != this.shoot) {
+ this.object[(this.shoot ? 'add' : 'remove') + 'Class']('highlight')
+ this.oldShoot = this.shoot
+ }
+ if (this.oldName != this.name) {
+ this.oldName = this.name
+ }
+ return this
+}
+
+Circle.prototype.destroy = function() {
+ this.object.remove()
+}
+
+// The player!
+var Player = exports.Player = function() {
+ Circle.apply(this, arguments)
+}
+util.inherits(Player, Circle)
+
+// The ball!
+var Ball = exports.Ball = function() {
+ Circle.apply(this, arguments)
+ this.id = 'ball'
+ this.register({ input: { collision: false } })
+ this.color = [ 255, 255, 255 ]
+ this.f = 0.995
+ this.width = 40
+ this.height = 40
+ this.bounce = 0.85
+}
+
+util.inherits(Ball, Circle)
View
119 examples/index.html
@@ -1,119 +0,0 @@
-<!doctype html>
-<html>
-<head>
-<title>maga</title>
-<script src="/exposed.js"></script>
-<script src="/jquery.js"></script>
-<script src="/socket.io/socket.io.js"></script>
-<script>
-
-$(function() {
-
-var util = require('util')
- , Maga = require('maga')
- , Circles = require('circles')
- , config = require('config')
-
-var socket = new io.Socket(config.host)
-
-var game = new Maga.Game({
- frameTime : 1000 / 45
- , loopTime : 1000 / 135
- , maxFrameTime : 1000 / 45
- , syncTime : 1000 / 20
- })
- , channel = game.createChannel()
- , protocol = new Maga.Protocol(game, channel)
- , myId
- , me
- , players = {}
- , $console = $('#console')
-
-socket.on('connect', function() {
- myId = parseInt(socket.transport.sessionid, 10).toString(32)
- me = new Circles.Player(myId).create()
-
- players[myId] = me
- channel.addObject(me)
-
- $('body').mousemove(function(ev) {
- me.move(ev.pageX, ev.pageY)
- })
-
- /*
- $('body').click(function() {
- game.set('frameTime', Math.max(game.get('frameTime') + (Math.random() * 300 - 260), 1000 / 30))
- })
- */
-
- channel.loop(function() {
- //$console.text(this.state.frame)
- })
-
- var str
- setInterval(function() {
- str = protocol.stringify(myId)
- if (str && str.length) socket.send(str)
- }, game.syncTime)
-})
-socket.on('message', function(message) {
- //console.log(message)
- var state = protocol.parse(message)
- for (var frame in state) {
- $console.text((frame > 0 && channel.state.frame - frame) + ' ' + channel.state.frame)
- for (var id in state[frame]) {
- if (id == myId) {
- delete state[frame][id]
- continue
- } else if (id == 'disconnectId') {
- console.log('DISCONNECTED', state[frame].disconnectId)
- return channel.removeObject(state[frame].disconnectId)
- } else if (!players.hasOwnProperty(id)) {
- console.log('CREATING PLAYER ' + id)
- players[id] = new Circles.Player(id).create()
- channel.addObject(players[id])
- }
- }
- }
- protocol.applyState(myId, state)
-})
-socket.on('disconnect', function() {})
-
-socket.connect()
-
-})
-</script>
-
-<style>
-html, body {
-width:100%;
-height:100%;
-overflow:hidden;
-background:#070707;
-}
-.circle {
-position:absolute;
-/* width:20px;
-height:20px;
-margin-left:-10px;
-margin-top:-10px; */
-border-radius:1000px;
-}
-#console {
-position:fixed;
-top:0;
-left:0;
-z-index:10000;
-color:#BBB;
-font-family:Consolas;
-width:300px;
-height:200px;
-}
-</style>
-
-</head>
-<body>
-<div id="console"></div>
-<a href="http://github.com/stagas/maga" target="_blank"><img style="position: absolute; top: 0; right: 0; border: 0; z-index:10000;" src="https://d3nwyuy0nl342s.cloudfront.net/img/4c7dc970b89fd04b81c8e221ba88ff99a06c6b61/687474703a2f2f73332e616d617a6f6e6177732e636f6d2f6769746875622f726962626f6e732f666f726b6d655f72696768745f77686974655f6666666666662e706e67" alt="Fork me on GitHub"></a>
-</body>
-</html>
View
166 lib/events.js
@@ -0,0 +1,166 @@
+if ('undefined' === typeof process) process = {};
+if (!process.EventEmitter) process.EventEmitter = function () {};
+
+var EventEmitter = exports.EventEmitter = process.EventEmitter;
+var isArray = Array.isArray;
+
+// By default EventEmitters will print a warning if more than
+// 10 listeners are added to it. This is a useful default which
+// helps finding memory leaks.
+//
+// Obviously not all Emitters should be limited to 10. This function allows
+// that to be increased. Set to zero for unlimited.
+var defaultMaxListeners = 10;
+EventEmitter.prototype.setMaxListeners = function(n) {
+ this._events.maxListeners = n;
+};
+
+
+EventEmitter.prototype.emit = function(type) {
+ // If there is no 'error' event listener then throw.
+ if (type === 'error') {
+ if (!this._events || !this._events.error ||
+ (isArray(this._events.error) && !this._events.error.length))
+ {
+ if (arguments[1] instanceof Error) {
+ throw arguments[1]; // Unhandled 'error' event
+ } else {
+ throw new Error("Uncaught, unspecified 'error' event.");
+ }
+ return false;
+ }
+ }
+
+ if (!this._events) return false;
+ var handler = this._events[type];
+ if (!handler) return false;
+
+ if (typeof handler == 'function') {
+ switch (arguments.length) {
+ // fast cases
+ case 1:
+ handler.call(this);
+ break;
+ case 2:
+ handler.call(this, arguments[1]);
+ break;
+ case 3:
+ handler.call(this, arguments[1], arguments[2]);
+ break;
+ // slower
+ default:
+ var args = Array.prototype.slice.call(arguments, 1);
+ handler.apply(this, args);
+ }
+ return true;
+
+ } else if (isArray(handler)) {
+ var args = Array.prototype.slice.call(arguments, 1);
+
+ var listeners = handler.slice();
+ for (var i = 0, l = listeners.length; i < l; i++) {
+ listeners[i].apply(this, args);
+ }
+ return true;
+
+ } else {
+ return false;
+ }
+};
+
+// EventEmitter is defined in src/node_events.cc
+// EventEmitter.prototype.emit() is also defined there.
+EventEmitter.prototype.addListener = function(type, listener) {
+ if ('function' !== typeof listener) {
+ throw new Error('addListener only takes instances of Function');
+ }
+
+ if (!this._events) this._events = {};
+
+ // To avoid recursion in the case that type == "newListeners"! Before
+ // adding it to the listeners, first emit "newListeners".
+ this.emit('newListener', type, listener);
+
+ if (!this._events[type]) {
+ // Optimize the case of one listener. Don't need the extra array object.
+ this._events[type] = listener;
+ } else if (isArray(this._events[type])) {
+
+ // Check for listener leak
+ if (!this._events[type].warned) {
+ var m;
+ if (this._events.maxListeners !== undefined) {
+ m = this._events.maxListeners;
+ } else {
+ m = defaultMaxListeners;
+ }
+
+ if (m && m > 0 && this._events[type].length > m) {
+ this._events[type].warned = true;
+ console.error('(node) warning: possible EventEmitter memory ' +
+ 'leak detected. %d listeners added. ' +
+ 'Use emitter.setMaxListeners() to increase limit.',
+ this._events[type].length);
+ console.trace();
+ }
+ }
+
+ // If we've already got an array, just append.
+ this._events[type].push(listener);
+ } else {
+ // Adding the second element, need to change to array.
+ this._events[type] = [this._events[type], listener];
+ }
+
+ return this;
+};
+
+EventEmitter.prototype.on = EventEmitter.prototype.addListener;
+
+EventEmitter.prototype.once = function(type, listener) {
+ var self = this;
+ self.on(type, function g() {
+ self.removeListener(type, g);
+ listener.apply(this, arguments);
+ });
+
+ return this;
+};
+
+EventEmitter.prototype.removeListener = function(type, listener) {
+ if ('function' !== typeof listener) {
+ throw new Error('removeListener only takes instances of Function');
+ }
+
+ // does not use listeners(), so no side effect of creating _events[type]
+ if (!this._events || !this._events[type]) return this;
+
+ var list = this._events[type];
+
+ if (isArray(list)) {
+ var i = list.indexOf(listener);
+ if (i < 0) return this;
+ list.splice(i, 1);
+ if (list.length == 0)
+ delete this._events[type];
+ } else if (this._events[type] === listener) {
+ delete this._events[type];
+ }
+
+ return this;
+};
+
+EventEmitter.prototype.removeAllListeners = function(type) {
+ // does not use listeners(), so no side effect of creating _events[type]
+ if (type && this._events && this._events[type]) this._events[type] = null;
+ return this;
+};
+
+EventEmitter.prototype.listeners = function(type) {
+ if (!this._events) this._events = {};
+ if (!this._events[type]) this._events[type] = [];
+ if (!isArray(this._events[type])) {
+ this._events[type] = [this._events[type]];
+ }
+ return this._events[type];
+};
View
32 lib/helpers.js
@@ -0,0 +1,32 @@
+// helpers
+
+exports.cosa = function(a) {
+ return Math.round(Math.cos(a * (Math.PI / 180)) * 10000) / 10000
+}
+
+exports.sina = function(a) {
+ return Math.round(Math.sin(a * (Math.PI / 180)) * 10000) / 10000
+}
+
+exports.circleToCircle = function(a, b, distOffset) {
+ var ret = { collided: false }
+ distOffset = distOffset || 0
+ var ho = distOffset / 2
+ ret.dx = (b.x - ho) - (a.x - ho)
+ ret.dy = (b.y - ho) - (a.y - ho)
+ ret.dist = Math.sqrt(ret.dx*ret.dx + ret.dy*ret.dy)
+ ret.minDist = (a.width / 2 + b.width / 2) + distOffset
+ if (ret.dist < ret.minDist) {
+ ret.collided = true
+ ret.angle = Math.atan2(ret.dy, ret.dx)
+ ret.tx = (a.x - ho) + (Math.cos(ret.angle) * ret.minDist)
+ ret.ty = (a.y - ho) + (Math.sin(ret.angle) * ret.minDist)
+ ret.sx = ret.tx - (b.x - ho)
+ ret.sy = ret.ty - (b.y - ho)
+ }
+ return ret
+}
+
+exports.randOrd = function() {
+ return Math.round(Math.random()) - 0.5
+}
View
0 examples/keys.js → lib/keys.js
File renamed without changes.
View
316 maga.js
@@ -2,87 +2,42 @@
* mAKE a gaME (maga)
*
* by stagas (gstagas@gmail.com)
+ *
+ * MIT licenced
*/
var util = require('util')
+ , EventEmitter = require('events').EventEmitter
var Maga = {}
-/*
- * Protocol
- */
-Maga.Protocol = function(game, channel) {
- this.game = game
- this.channel = channel || game.createChannel()
- game.protocol = this
- this.statePrevious = {}
- this.frames = [ 0 ]
-
- var self = this
- setInterval(function() {
- var arr = self.frames.length > 100 && self.frames || [ 0 ]
- var setFrame = Math.floor(self.frames.reduce(function(n, t) { return parseInt(n, 10) + t }) / self.frames.length)
- if (!isNaN(setFrame) && setFrame > 0) {
- self.channel.state.frame = setFrame + Math.floor(Math.abs(self.channel.state.frame - setFrame) / 2)
- console.log('REDUCED:', self.channel.state.frame)
- }
- }, 1000)
- return this
-}
-
-// Serialize object <id> state
-Maga.Protocol.prototype.stringify = function(id) {
- if (!this.channel.state.frame) return ''
- var obj = {}, state = {}
- state[id] = this.channel.state.current && this.channel.state.current[id]
- var diff = this.channel.state.compare.call(this.channel.state, state, this.statePrevious, ['input'])
- var str = ''
- if (diff.changes) {
- obj[this.channel.state.frame || 0] = state || {}
- str = JSON.stringify(obj)
- this.statePrevious = state
- //console.log('STRINGIFIED CURRENT:', str)
- }
- return str
-}
-
- // Parse state object
-Maga.Protocol.prototype.parse = function(stringified) {
- var state = JSON.parse(stringified)
- //console.log('RECEIVED STRINGIFIED:', state)
- return state
-}
-
-// Apply state to game excluding myId
-// TODO: this needs fixing
-Maga.Protocol.prototype.applyState = function(myId, state) {
- this.frames.concat(Object.keys(state))
- if (this.frames.length > 500) {
- while (this.frames.length > 500) {
- this.frames.shift()
- }
- }
- for (var frame in state) {
- if (!frame || isNaN(frame)) frame = 0
- var newState = this.channel.state.replay.call(this.channel.state, myId, frame, state[frame])
- this.channel.state.set(newState)
- }
-}
+var debug
/*
* Game
*/
-Maga.Game = function(options) {
- this.name = 'Maga Game'
+Maga.Game = function(name, options) {
+ if ('object' === typeof name)
+ options = name, name = 'Maga Game'
+
+ this.name = name
this.frameTime = 1000 / 45
this.loopTime = 1000 / 135
this.maxFrameTime = 1000 / 45
- this.syncTime = 1000 / 5
+ this.syncTime = 1000 / 15
+ this.lag = 110
+ this.draw = ('undefined' !== typeof window)
+ this.debug = 0
for (var k in options) {
this[k] = options[k]
}
- this.channels = {}
- this.protocol = null
+
+ this.rooms = {}
+
+ if (this.debug) debug = function(level) {
+ if (level <= this.debug)
+ console.log.apply(console, [].slice.call(arguments, 1))
+ }
}
// Get an option value
@@ -95,56 +50,137 @@ Maga.Game.prototype.set = function(key, val) {
return this[key] = val
}
-// Create a channel of ID
-Maga.Game.prototype.createChannel = function(id) {
+// Create a room of ID
+Maga.Game.prototype.createRoom = function(id) {
if (!id) id = Math.random() * 100000 | 0
- this.channels[id] = new Maga.Channel(id, this)
- return this.channels[id]
+ this.rooms[id] = new Maga.Room(id, this)
+ return this.rooms[id]
}
-// Destroy a channel by ID or object
-Maga.Game.prototype.destroyChannel = function(channel) {
- if ('object' !== typeof channel) channel = this.channels[channel]
- channel.destroy()
+// Destroy a room by ID or object
+Maga.Game.prototype.destroyRoom = function(room) {
+ if ('object' !== typeof room) room = this.rooms[room]
+ room.destroy()
return this
}
/*
- * Channel
+ * Protocol
+ */
+Maga.Protocol = function(room) {
+ this.room = room
+ this.state = room.state
+ this.statePrevious = {}
+ return this
+}
+
+// Serialize object <id> state
+Maga.Protocol.prototype.stringify = function(id, force) {
+ if ('object' === typeof id) id = id.id
+ var obj = {}, state = {}
+ state[id] = this.room.objects && id && this.room.objects[id] && this.room.objects[id].state()
+ var diff = this.state.compare.call(this.state, state, this.statePrevious, ['input'])
+ var str = ''
+ if (diff.changes || force) {
+ //if (!force)
+ //diff = this.state.compare.call(this.state, state, this.statePrevious)
+
+ obj[this.state.frame || 0] = state || {}
+ str = JSON.stringify(obj)
+ this.statePrevious = state
+ //console.log('STRINGIFIED CURRENT:', str)
+ }
+ return str
+}
+
+// Parse state object
+Maga.Protocol.prototype.parse = function(stringified, cb) {
+ var state = JSON.parse(stringified)
+ this.room.emit('state', state)
+ //console.log('RECEIVED STRINGIFIED:', state)
+ cb && cb(state)
+ return state
+}
+
+/*
+ * Room
*/
-Maga.Channel = function(id, game) {
+Maga.Room = function(id, game) {
+ var self = this
this.id = id
this.game = game
this.objects = {}
this.state = new Maga.State(this)
+ this.protocol = new Maga.Protocol(this)
+ this.watching = {}
+ setInterval(function() {
+ var str, cb
+ for (var id in self.watching) {
+ cb = self.watching[id]
+ str = self.stringify(id)
+ if (str && str.length) {
+ self.emit('sync', id, str)
+ cb && cb(str)
+ }
+ }
+ }, this.game.syncTime)
+ EventEmitter.call(this)
+}
+
+util.inherits(Maga.Room, EventEmitter)
+
+Maga.Room.prototype.stringify = function() {
+ return this.protocol.stringify.apply(this.protocol, arguments)
+}
+
+Maga.Room.prototype.parse = function() {
+ return this.protocol.parse.apply(this.protocol, arguments)
}
-// Destroy channel and remove from game
-Maga.Channel.prototype.destroy = function() {
+// Destroy room and remove from game
+Maga.Room.prototype.destroy = function() {
for (var id in this.objects) {
delete this.objects[id]
}
- delete this.game.channels[this.id]
+ delete this.game.rooms[this.id]
return this
}
-// Add an object to the channel
-Maga.Channel.prototype.addObject = function(obj) {
- obj.channel = this
+// Add an object to the room
+Maga.Room.prototype.addObject = function(obj) {
+ obj.room = this
this.objects[obj.id] = obj
return this
}
-// Remove an object from the channel
-Maga.Channel.prototype.removeObject = function(obj) {
- if ('object' !== typeof obj) obj = this.objects[obj]
- obj && obj.destroy()
- delete this.objects[obj.id]
+// Remove an object from the room
+Maga.Room.prototype.removeObject = function(id) {
+ if ('object' === typeof id) id = id.id
+ this.game.draw && this.objects[id] && this.objects[id].destroy()
+ delete this.objects[id]
return this
}
-// Main channel game loop
-Maga.Channel.prototype.loop = function(fn) {
+Maga.Room.prototype.applyState = function(state) {
+ for (var frame in state) {
+ if (!frame || isNaN(frame)) continue
+ var newState = this.state.replay(frame, state[frame])
+ this.state.set(newState)
+ }
+}
+
+Maga.Room.prototype.watch = function(id, cb) {
+ if ('object' === typeof id) id = id.id
+ this.watching[id] = cb
+}
+
+Maga.Room.prototype.unwatch = function(id) {
+ if ('object' === typeof id) id = id.id
+ delete this.watching[id]
+}
+
+// Main room game loop
+Maga.Room.prototype.loop = function(fn) {
var self = this
self.state.tick()
fn && fn.call(self)
@@ -156,9 +192,9 @@ Maga.Channel.prototype.loop = function(fn) {
/*
* Object
*/
-Maga.Object = function(id, channel) {
+Maga.Object = function(id, room) {
this.id = id
- this.channel = channel
+ this.room = room
// Our property types
this.properties = {
@@ -178,7 +214,7 @@ Maga.Object.prototype.register = function(obj) {
for (var p in obj[type]) {
this[p] = obj[type][p]
this.properties[type].push(p)
- this.properties.all.push(p)
+ if (type !== 'static') this.properties.all.push(p)
}
}
return this
@@ -216,6 +252,8 @@ Maga.Object.prototype.applyState = function(state, types) {
/*
* Timer
*/
+
+// NOTE: Maybe all objects should have a timer instance
Maga.Timer = function(game, target) {
this.game = game
this.now = Date.now()
@@ -245,32 +283,28 @@ Maga.Timer.prototype.alpha = function() {
/*
* State
*/
-Maga.State = function(channel) {
- this.channel = channel
+Maga.State = function(room) {
+ this.room = room
+ this.game = room.game
// Current and previous state objects
this.current = {}
this.previous = {}
// Frame position
- this.frame = 0
+ this.frame = 1
// History object
this.history = {}
// Init timer
- this.timer = new Maga.Timer(channel.game)
-
- var self = this
- setTimeout(function() {
- if (!self.frame || isNaN(self.frame)) self.frame = 1
- }, 5000)
+ this.timer = new Maga.Timer(room.game)
}
// Update state of all our objects (move 1 frame forward)
Maga.State.prototype.update = function() {
- for (var id in this.channel.objects) {
- this.channel.objects[id].update()
+ for (var id in this.room.objects) {
+ this.room.objects[id].update()
}
return this
}
@@ -282,23 +316,23 @@ Maga.State.prototype.advance = function(n) {
}
}
-// Get entire channel current state
+// Get entire room current state
Maga.State.prototype.get = function() {
var state = {}
- for (var id in this.channel.objects) {
- state[id] = this.channel.objects[id].state()
+ for (var id in this.room.objects) {
+ state[id] = this.room.objects[id].state()
}
return state
}
-// Apply an entire channel state
+// Apply an entire room state
Maga.State.prototype.set = function(state, types) {
for (var id in state) {
- if (this.channel.objects.hasOwnProperty(id))
- this.channel.objects[id].applyState(state[id], types)
+ if (this.room.objects.hasOwnProperty(id))