joyce
Description
Joyce is a multiplayer game server framework built on top of Metacosm
What is the big idea?
The idea is to build ‘isomorphic’ Ruby games: to target both a Gosu game client as well as a game server running in the cloud. Both are running the same code, but the responsibilities are split:
- The game server runs game business logic, and publishes events that the clients consume to hydrate their local views
- Clients publish commands the the server validates and handles, updating game state and generating events
One upshot of this is that all game processing is kept on the server, and clients are doing nothing but rendering views and when necessary figuring out what commands to publish (when the user interacts).
Features
Examples
Below is the source for a toy app that displays a list of other connected players and how long they’ve been connected. The server uses a ping command from clients to determine if players have disconnected.
require 'joyce'
module Example
class SampleAppView < Joyce::ApplicationView
def render
game_view.render(window, font)
end
def game_view
GameView.find_by active_player_id: application.player_id
end
end
class SampleServer < Joyce::Server
def setup
Game.create
end
def tick
Game.all.each(&:iterate!)
end
end
class Application < Joyce::Application
viewed_with Example::SampleAppView
def setup
GameView.create(active_player_id: player_id)
sim.params[:active_player_id] = player_id
end
def tick
@ticks ||= 0
@ticks += 1
# p [ :sample_app_tick ]
if (@ticks % 30 == 0)
fire(PingCommand.create(player_id: player_id, player_name: player_name))
end
end
def player_id
@player_id ||= SecureRandom.uuid
end
def player_name
@player_name ||= %w[ Alice Bob Cardiff Danielle Echo Fargo ].sample
end
end
class BulletPointAtom
def render(color: 0xf0f0f0f0, location:,size: 10,window:)
x,y = location
window.draw_quad(x,y,color,
x,y+size,color,
x+size,y,color,
x+size,y+size,color)
end
end
class TextBoxAtom
def render(message:, location:, font:)
x,y = *location
font.draw(, x, y, 1)
end
end
class ListMolecule
def render(title: '', elements:, location:, font:, window:)
x0,y0 = location
font.draw(title, x0, y0, 1)
elements.each_with_index do |element, index|
x, y = x0, y0 + ((index+1)*20)
bullet_point = BulletPointAtom.new
bullet_point.render(location: [x-10,y], window: window)
text_box = TextBoxAtom.new
text_box.render(message: element, location: [x,y], font: font)
end
end
end
class PlayerView < Metacosm::View
belongs_to :game_view
attr_accessor :name, :joined_at, :player_id
end
class GameView < Metacosm::View
has_many :player_views
attr_accessor :active_player_id, :game_id
# attr_accessor :player_names
def render(window, font)
# p [ :render_game_view, pinged_at: pinged_at ]
window.draw_quad(10,10, 0xf0f0f0f0,
10,20, 0xf0f0f0f0,
20,10, 0xf0f0f0f0,
20,20, 0xf0f0f0f0)
if player_names && player_names.any?
ListMolecule.new.render(
title: "Connected Users:",
elements: player_names,
location: [40,40],
font: font,
window: window
)
end
end
private
def player_names
self.player_views.map { |p| "#{p.name} (#{time_ago_in_words(p.joined_at)})" }
end
end
class PingCommand < Metacosm::Command
attr_accessor :player_id, :player_name
end
class PingCommandHandler
def handle(player_id:, player_name:)
game = Game.find_by(players: { id: player_id })
if game.nil?
game = Game.first || Game.create
game.admit_player(player_id: player_id, player_name: player_name)
end
game.ping(player_id: player_id)
end
end
class ApplicationEventListener < Metacosm::EventListener
def game_view
GameView.find_by(active_player_id: active_player_id)
end
def active_player_id
self.simulation.params[:active_player_id]
end
end
class PlayerAdmittedEvent < Metacosm::Event
attr_accessor :player_id, :player_name, :connected_player_list
end
class PlayerAdmittedEventListener < ApplicationEventListener
def receive(player_id:, player_name:, connected_player_list:)
if game_view
connected_player_list.each do |id:, name:, joined_at:|
player_view = game_view.player_views.where(player_id: id).first_or_create
player_view.update(name: name, joined_at: joined_at)
end
end
end
end
class PlayerDroppedEvent < Metacosm::Event
attr_accessor :player_id, :connected_player_list
end
class PlayerDroppedEventListener < ApplicationEventListener
def receive(player_id:, connected_player_list:)
if game_view
game_view.player_views.map(&:destroy)
connected_player_list.each do |id:, name:, joined_at:|
player_view = game_view.player_views.where(player_id: id).first_or_create
player_view.update(name: name, joined_at: joined_at)
end
end
end
end
end
Requirements
Install
$ gem install joyce
Synopsis
$ joyce
Copyright
Copyright (c) 2016 Joseph Weissman
See LICENSE for details.