game_2d 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +15 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +84 -0
- data/Rakefile +1 -0
- data/bin/game_2d_client.rb +17 -0
- data/bin/game_2d_server.rb +21 -0
- data/game_2d.gemspec +32 -0
- data/lib/game_2d/client_connection.rb +127 -0
- data/lib/game_2d/client_engine.rb +227 -0
- data/lib/game_2d/complex_move.rb +45 -0
- data/lib/game_2d/entity.rb +371 -0
- data/lib/game_2d/entity/block.rb +73 -0
- data/lib/game_2d/entity/owned_entity.rb +29 -0
- data/lib/game_2d/entity/pellet.rb +27 -0
- data/lib/game_2d/entity/titanium.rb +11 -0
- data/lib/game_2d/entity_constants.rb +14 -0
- data/lib/game_2d/game.rb +213 -0
- data/lib/game_2d/game_space.rb +462 -0
- data/lib/game_2d/game_window.rb +260 -0
- data/lib/game_2d/hash.rb +11 -0
- data/lib/game_2d/menu.rb +82 -0
- data/lib/game_2d/move/rise_up.rb +77 -0
- data/lib/game_2d/player.rb +251 -0
- data/lib/game_2d/registerable.rb +25 -0
- data/lib/game_2d/serializable.rb +69 -0
- data/lib/game_2d/server_connection.rb +104 -0
- data/lib/game_2d/server_port.rb +74 -0
- data/lib/game_2d/storage.rb +42 -0
- data/lib/game_2d/version.rb +3 -0
- data/lib/game_2d/wall.rb +21 -0
- data/lib/game_2d/zorder.rb +3 -0
- data/media/Beep.wav +0 -0
- data/media/Space.png +0 -0
- data/media/Star.png +0 -0
- data/media/Starfighter.bmp +0 -0
- data/media/brick.gif +0 -0
- data/media/cement.gif +0 -0
- data/media/crosshair.gif +0 -0
- data/media/dirt.gif +0 -0
- data/media/pellet.png +0 -0
- data/media/pellet.xcf +0 -0
- data/media/player.png +0 -0
- data/media/player.xcf +0 -0
- data/media/rock.png +0 -0
- data/media/rock.xcf +0 -0
- data/media/steel.gif +0 -0
- data/media/tele.gif +0 -0
- data/media/titanium.gif +0 -0
- data/media/unlikelium.gif +0 -0
- data/spec/client_engine_spec.rb +235 -0
- data/spec/game_space_spec.rb +347 -0
- metadata +246 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Greg Meyers
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
# Game2d
|
2
|
+
|
3
|
+
A 2D sandbox game, using Gosu and REnet.
|
4
|
+
|
5
|
+
There's not much here yet, in terms of actual gameplay. At this point I'm laying the foundation. What I have so far:
|
6
|
+
|
7
|
+
* The server runs at 60 frames a second, to match Gosu.
|
8
|
+
* A GameSpace holds a copy of the game's state. This is primarily a grid populated by Entities.
|
9
|
+
* The dimensions of the GameSpace (in cells) are specified on server startup.
|
10
|
+
* Each cell in the grid is 40x40 pixels, but is represented as 400x400 internally. This allows for motion at slow speeds (1 pixel every 10 ticks == 6 pixels per second).
|
11
|
+
* Entity positions and velocities are integers. Apart from player input, all entities' behaviors are predictable. This allows the client to make accurate predictions of the server state.
|
12
|
+
* New Entities are assigned IDs in order, again, to allow the client to predict which ID will be assigned to each new entity.
|
13
|
+
* Clients connect using REnet. The handshaking process creates a Player object to represent the client.
|
14
|
+
* Keyboard commands control the player:
|
15
|
+
* A and D, or LeftArrow and RightArrow: Hold down to accelerate.
|
16
|
+
* LeftControl or RightControl: Hold down to brake (slow down).
|
17
|
+
* S, or DownArrow: Hold down to build (the longer you hold, the stronger the block).
|
18
|
+
* W, or UpArrow:
|
19
|
+
* When not building: Flip (turn 180 degrees).
|
20
|
+
* When building: Rise up (ascend to the top of the built block).
|
21
|
+
* Left button: Fire a pellet. Mouse click determines the direction and speed -- closer to the player means slower speed.
|
22
|
+
* Right button: Build a block where you click. *(This will eventually become part of a level-editing mode.)*
|
23
|
+
* A simple menu system is also available. Left-click to select. Esc returns to the top-level.
|
24
|
+
* The menus include a Save feature, which tells the server to save the level's state.
|
25
|
+
* Levels are persisted in ~/.game_2d/<level name> as JSON text.
|
26
|
+
* On subsequent startup with the same name, the level is loaded.
|
27
|
+
* The client and server communicate using JSON text. This is undoubtedly inefficient, but aids in debugging at this early stage.
|
28
|
+
* Every frame is numbered by the server, and is referred to as a 'tick'.
|
29
|
+
* Player actions are both executed locally, and sent to the server.
|
30
|
+
* If other players are connected, the server sends everyone's actions to everyone else.
|
31
|
+
* Player actions are dated six ticks in the future, to give everyone a chance to get the message early. That way everyone can execute the action at the same time.
|
32
|
+
* The server broadcasts the complete GameSpace four times a second, just in case a client gets out of sync.
|
33
|
+
* The client predicts the server state, but treats its own copy as advisory only -- the server's is definitive.
|
34
|
+
* If the server sends something conflicting, the client discards its wrong prediction.
|
35
|
+
* This is intended to compensate for dropped packets or lag, but much more testing is needed.
|
36
|
+
|
37
|
+
The physics is (intentionally) pretty simple. Unsupported objects fall, accelerating downward at a rate of 1/10 pixel per tick per tick. Blocks assume one of the following forms depending on how many hit points they possess:
|
38
|
+
|
39
|
+
* Dirt blocks (1-5 HP) fall unless something is underneath.
|
40
|
+
* Brick blocks (6-10 HP) can stay up if supported from both left and right.
|
41
|
+
* Cement blocks (11-15 HP) can stay up if supported from *either* left *or* right.
|
42
|
+
* Steel blocks (16-20 HP) can stay up if touching anything else, even above.
|
43
|
+
* Unlikelium blocks (21-25 HP) never fall.
|
44
|
+
|
45
|
+
When building a block, the longer you hold the S or DownArrow key, the more HP will be awarded to the block. Hitting a block with a pellet reduces its HP, until it degrades to a lower form. Dirt blocks with 1 HP will be entirely destroyed when hit.
|
46
|
+
|
47
|
+
Whether an object is supported depends exclusively on its immediate surroundings. That means two objects can support each other, and hang suspended. For example, a dirt block sitting on a steel block will support each other. A horizontal row of brick, capped at either end with cement, will also be free-standing.
|
48
|
+
|
49
|
+
Pellets are fired from the player's center, and are affected by gravity. They damage whatever they hit first, and disappear. Pellets won't hit the player who fired them.
|
50
|
+
|
51
|
+
There are also Titanium entities, which never fall, and are indestructible. These are intended for use in designing levels with specific shapes. They can only be created by using the menu to select Titanium, and then right-clicking.
|
52
|
+
|
53
|
+
A player is considered to be supported if their "feet" are touching a block. Unsupported players will turn feet-downward and fall, until they land on something. Supported players may slide left or right, and will follow any edges they reach -- going up and down walls, or hanging under ceilings. The "flip" maneuver swaps head and feet; this becomes useful if the player's head is exactly touching another block. At all other times, a flip leads to a fall (which can be useful too).
|
54
|
+
|
55
|
+
When building a block, the player and the new block occupy the same space. This is allowed until the player moves off of the block. After that, the block is considered opaque as usual. While the block is still "inside" the player, the Rise Up maneuver may be used. This moves the player headward (which may not be "up"--it depends which way the player is turned) to sit "on top" of the block. It's possible to use this maneuver to construct a horizontal row of bricks, carefully.
|
56
|
+
|
57
|
+
The GameSpace is bounded by invisible, indestructible Wall entities. These can also support blocks, or the player.
|
58
|
+
|
59
|
+
|
60
|
+
## Installation
|
61
|
+
|
62
|
+
Install it with:
|
63
|
+
|
64
|
+
$ gem install game_2d
|
65
|
+
|
66
|
+
## Usage
|
67
|
+
|
68
|
+
This needs streamlining.
|
69
|
+
|
70
|
+
Start a server in one window:
|
71
|
+
|
72
|
+
$ game_2d_server.rb -w 50 -h 50 --level example
|
73
|
+
|
74
|
+
And a client in another window:
|
75
|
+
|
76
|
+
$ game_2d_client.rb --name Bender --hostname 127.0.0.1
|
77
|
+
|
78
|
+
## Contributing
|
79
|
+
|
80
|
+
1. Fork it ( https://github.com/sereneiconoclast/game_2d/fork )
|
81
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
82
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
83
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
84
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,17 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'rubygems'
|
3
|
+
require 'trollop'
|
4
|
+
require 'game_2d/game_window'
|
5
|
+
|
6
|
+
opts = Trollop::options do
|
7
|
+
opt :name, "Player name", :type => :string, :required => true
|
8
|
+
opt :hostname, "Hostname of server", :type => :string, :required => true
|
9
|
+
opt :port, "Port number", :default => DEFAULT_PORT
|
10
|
+
opt :profile, "Turn on profiling", :type => :boolean
|
11
|
+
opt :debug_traffic, "Debug network traffic", :type => :boolean
|
12
|
+
end
|
13
|
+
|
14
|
+
$debug_traffic = opts[:debug_traffic] || false
|
15
|
+
|
16
|
+
window = GameWindow.new( opts[:name], opts[:hostname], opts[:port], opts[:profile] )
|
17
|
+
window.show
|
@@ -0,0 +1,21 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'rubygems'
|
3
|
+
require 'trollop'
|
4
|
+
require 'game_2d/game'
|
5
|
+
|
6
|
+
opts = Trollop::options do
|
7
|
+
opt :level, "Level name", :type => :string, :required => true
|
8
|
+
opt :width, "Level width", :type => :integer
|
9
|
+
opt :height, "Level height", :type => :integer
|
10
|
+
opt :port, "Port number", :type => :integer
|
11
|
+
opt :storage, "Data storage dir (in home directory)", :type => :string
|
12
|
+
opt :max_clients, "Maximum clients", :type => :integer
|
13
|
+
opt :self_check, "Run data consistency checks", :type => :boolean
|
14
|
+
opt :profile, "Turn on profiling", :type => :boolean
|
15
|
+
opt :debug_traffic, "Debug network traffic", :type => :boolean
|
16
|
+
opt :registry_broadcast_every, "Send registry broadcasts every N frames (0 = never)", :type => :integer
|
17
|
+
end
|
18
|
+
|
19
|
+
$debug_traffic = opts[:debug_traffic] || false
|
20
|
+
|
21
|
+
Game.new(opts).run
|
data/game_2d.gemspec
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'game_2d/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "game_2d"
|
8
|
+
spec.version = Game2d::VERSION
|
9
|
+
spec.authors = ["Greg Meyers"]
|
10
|
+
spec.email = ["cmdr.samvimes@gmail.com"]
|
11
|
+
spec.summary = %q{Client/server sandbox game using Gosu and REnet}
|
12
|
+
spec.description = %q{Client/server sandbox game using Gosu and REnet}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
spec.required_ruby_version = ">= 1.9.3"
|
21
|
+
|
22
|
+
spec.add_runtime_dependency "facets", [">= 2.9.3"]
|
23
|
+
spec.add_runtime_dependency "gosu", [">= 0.8.5"]
|
24
|
+
spec.add_runtime_dependency "json", [">= 1.8.1"]
|
25
|
+
spec.add_runtime_dependency "renet", [">= 0.1.14"]
|
26
|
+
spec.add_runtime_dependency "trollop", [">= 2.0"]
|
27
|
+
|
28
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
29
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
30
|
+
spec.add_development_dependency "rr", "~> 1.1.2"
|
31
|
+
spec.add_development_dependency "rspec", "~> 3.1.0"
|
32
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
require 'renet'
|
2
|
+
require 'json'
|
3
|
+
require 'game_2d/hash'
|
4
|
+
|
5
|
+
# The client creates one of these.
|
6
|
+
# It is then used for all communication with the server.
|
7
|
+
|
8
|
+
class ClientConnection
|
9
|
+
attr_reader :player_name
|
10
|
+
attr_accessor :engine
|
11
|
+
|
12
|
+
# We tell the server to execute all actions this many ticks
|
13
|
+
# in the future, to give the message time to propagate around
|
14
|
+
# the fleet
|
15
|
+
ACTION_DELAY = 6 # 1/10 of a second
|
16
|
+
|
17
|
+
def initialize(host, port, game, player_name, timeout=2000)
|
18
|
+
# remote host address, remote host port, channels, download bandwidth, upload bandwidth
|
19
|
+
@socket = _create_connection(host, port, 2, 0, 0)
|
20
|
+
@game = game
|
21
|
+
@player_name = player_name
|
22
|
+
|
23
|
+
@socket.on_connection(method(:on_connect))
|
24
|
+
@socket.on_disconnection(method(:on_close))
|
25
|
+
@socket.on_packet_receive(method(:on_packet))
|
26
|
+
|
27
|
+
@socket.connect(timeout)
|
28
|
+
end
|
29
|
+
|
30
|
+
def _create_connection(*args)
|
31
|
+
ENet::Connection.new(*args)
|
32
|
+
end
|
33
|
+
|
34
|
+
def on_connect
|
35
|
+
puts "Connected to server - sending handshake"
|
36
|
+
# send handshake reliably
|
37
|
+
send_record( { :handshake => { :player_name => @player_name } }, true)
|
38
|
+
end
|
39
|
+
|
40
|
+
def on_close
|
41
|
+
puts "Client disconnected by server"
|
42
|
+
@game.shutdown
|
43
|
+
end
|
44
|
+
|
45
|
+
def on_packet(data, channel)
|
46
|
+
hash = JSON.parse(data).fix_keys
|
47
|
+
debug_packet('Received', hash)
|
48
|
+
|
49
|
+
pong = hash[:pong]
|
50
|
+
if pong
|
51
|
+
stop = Time.now.to_f
|
52
|
+
puts "Ping took #{stop - pong[:start]} seconds"
|
53
|
+
end
|
54
|
+
|
55
|
+
at_tick = hash[:at_tick]
|
56
|
+
fail "No at_tick in #{hash.inspect}" unless at_tick
|
57
|
+
|
58
|
+
world = hash[:world]
|
59
|
+
if world
|
60
|
+
@engine.establish_world(world, at_tick)
|
61
|
+
end
|
62
|
+
|
63
|
+
delta_keys = [
|
64
|
+
:add_players, :add_npcs, :delete_entities, :update_entities, :update_score, :move
|
65
|
+
]
|
66
|
+
@engine.add_delta(hash) if delta_keys.any? {|k| hash.has_key? k}
|
67
|
+
|
68
|
+
you_are = hash[:you_are]
|
69
|
+
if you_are
|
70
|
+
# The 'world' response includes deltas for add_players and add_npcs
|
71
|
+
# Need to process those first, as one of the players is us
|
72
|
+
@engine.apply_all_deltas(at_tick)
|
73
|
+
|
74
|
+
@engine.create_local_player you_are
|
75
|
+
end
|
76
|
+
|
77
|
+
registry, highest_id = hash[:registry], hash[:highest_id]
|
78
|
+
@engine.sync_registry(registry, highest_id, at_tick) if registry
|
79
|
+
end
|
80
|
+
|
81
|
+
def send_actions_at
|
82
|
+
@engine.tick + ACTION_DELAY
|
83
|
+
end
|
84
|
+
|
85
|
+
def send_move(move, args={})
|
86
|
+
return unless move
|
87
|
+
args[:move] = move.to_s
|
88
|
+
delta = { :at_tick => send_actions_at, :move => args }
|
89
|
+
send_record delta
|
90
|
+
delta[:move][:player_id] = @engine.player_id
|
91
|
+
@engine.add_delta delta
|
92
|
+
end
|
93
|
+
|
94
|
+
def send_create_npc(npc)
|
95
|
+
delta = { :at_tick => send_actions_at, :create_npc => npc }
|
96
|
+
send_record delta
|
97
|
+
@engine.add_delta delta
|
98
|
+
end
|
99
|
+
|
100
|
+
def send_save
|
101
|
+
send_record :save => true
|
102
|
+
end
|
103
|
+
|
104
|
+
def send_ping
|
105
|
+
send_record :ping => { :start => Time.now.to_f }
|
106
|
+
end
|
107
|
+
|
108
|
+
def send_record(data, reliable=false)
|
109
|
+
debug_packet('Sending', data)
|
110
|
+
@socket.send_packet(data.to_json, reliable, 0)
|
111
|
+
@socket.flush
|
112
|
+
end
|
113
|
+
|
114
|
+
def debug_packet(direction, hash)
|
115
|
+
return unless $debug_traffic
|
116
|
+
at_tick = hash[:at_tick] || 'NO TICK'
|
117
|
+
keys = hash.keys - [:at_tick]
|
118
|
+
puts "#{direction} #{keys.join(', ')} <#{at_tick}>"
|
119
|
+
end
|
120
|
+
|
121
|
+
def update
|
122
|
+
@socket.update(0) # non-blocking
|
123
|
+
end
|
124
|
+
|
125
|
+
def online?; @socket.online?; end
|
126
|
+
def disconnect; @socket.disconnect(200); end
|
127
|
+
end
|
@@ -0,0 +1,227 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'game_2d/game_space'
|
3
|
+
require 'game_2d/serializable'
|
4
|
+
|
5
|
+
# Server sends authoritative copy of GameSpace for tick T0.
|
6
|
+
# We store that, along with pending moves generated by
|
7
|
+
# our player, and pending moves for other players sent to us
|
8
|
+
# by the server. Then we calculate further ticks of action.
|
9
|
+
# These are predictions and might well be wrong.
|
10
|
+
#
|
11
|
+
# When the user requests an action at T0, we delay it by 100ms
|
12
|
+
# (T6). We tell the server about it immediately, but advise
|
13
|
+
# it not to perform the action until T6 arrives. The server
|
14
|
+
# rebroadcasts this information to other players. Hopefully,
|
15
|
+
# everyone receives all players' actions before T6.
|
16
|
+
#
|
17
|
+
# We render one tick after another, 60 per second, the same
|
18
|
+
# speed at which the server calculates them. But because we
|
19
|
+
# may get out of sync, we also watch for full server updates
|
20
|
+
# at, e.g., T15. When we get a new full update, we can discard
|
21
|
+
# all information about older ticks. Anything we've calculated
|
22
|
+
# past the new update must now be recalculated, applying again
|
23
|
+
# whatever pending player actions we have heard about.
|
24
|
+
class ClientEngine
|
25
|
+
# If we haven't received a full update from the server in this
|
26
|
+
# many ticks, stop guessing. We're almost certainly wrong by
|
27
|
+
# this point.
|
28
|
+
MAX_LEAD_TICKS = 30
|
29
|
+
|
30
|
+
attr_reader :tick
|
31
|
+
|
32
|
+
def initialize(game_window)
|
33
|
+
@game_window, @width, @height = game_window, 0, 0
|
34
|
+
@spaces = {}
|
35
|
+
@deltas = Hash.new {|h,tick| h[tick] = Array.new}
|
36
|
+
@earliest_tick = @tick = @preprocessed = nil
|
37
|
+
end
|
38
|
+
|
39
|
+
def establish_world(world, at_tick)
|
40
|
+
@world_name, @world_id = world[:world_name], world[:world_id]
|
41
|
+
@width, @height = world[:cell_width], world[:cell_height]
|
42
|
+
highest_id = world[:highest_id]
|
43
|
+
create_initial_space(at_tick, highest_id)
|
44
|
+
end
|
45
|
+
|
46
|
+
def create_initial_space(at_tick, highest_id)
|
47
|
+
@earliest_tick = @tick = at_tick
|
48
|
+
space = @spaces[@tick] = GameSpace.new.establish_world(@world_name, @world_id, @width, @height)
|
49
|
+
space.highest_id = highest_id
|
50
|
+
space
|
51
|
+
end
|
52
|
+
|
53
|
+
def space_at(tick)
|
54
|
+
return @spaces[tick] if @spaces[tick]
|
55
|
+
|
56
|
+
fail "Can't create space at #{tick}; earliest space we know about is #{@earliest_tick}" if tick < @earliest_tick
|
57
|
+
|
58
|
+
last_space = space_at(tick - 1)
|
59
|
+
@spaces[tick] = new_space = GameSpace.new.copy_from(last_space)
|
60
|
+
|
61
|
+
# Certain deltas, like add_npcs, need to be processed post-update
|
62
|
+
# to match the server's behavior. An object created during tick T
|
63
|
+
# does not receive its first update until T+1.
|
64
|
+
apply_deltas_before_update(tick)
|
65
|
+
new_space.update
|
66
|
+
apply_deltas_after_update(tick)
|
67
|
+
|
68
|
+
new_space
|
69
|
+
end
|
70
|
+
|
71
|
+
def update
|
72
|
+
return unless @tick # handshake not yet answered
|
73
|
+
|
74
|
+
# Display the frame we received from the server as-is
|
75
|
+
if @preprocessed == @tick
|
76
|
+
@preprocessed = nil
|
77
|
+
return space_at(@tick)
|
78
|
+
end
|
79
|
+
|
80
|
+
if @tick - @earliest_tick >= MAX_LEAD_TICKS
|
81
|
+
$stderr.puts "Lost connection? Running ahead of server?"
|
82
|
+
return space_at(@tick)
|
83
|
+
end
|
84
|
+
space_at(@tick += 1)
|
85
|
+
end
|
86
|
+
|
87
|
+
def space
|
88
|
+
@spaces[@tick]
|
89
|
+
end
|
90
|
+
|
91
|
+
def create_local_player(player_id)
|
92
|
+
old_player_id = @game_window.player_id
|
93
|
+
fail "Already have player #{old_player_id}!?" if old_player_id
|
94
|
+
|
95
|
+
@game_window.player_id = player_id
|
96
|
+
puts "I am player #{player_id}"
|
97
|
+
end
|
98
|
+
|
99
|
+
def player_id
|
100
|
+
@game_window.player_id
|
101
|
+
end
|
102
|
+
|
103
|
+
def add_delta(delta)
|
104
|
+
at_tick = delta.delete :at_tick
|
105
|
+
if at_tick < @tick
|
106
|
+
$stderr.puts "Received delta #{@tick - at_tick} ticks late"
|
107
|
+
if at_tick <= @earliest_tick
|
108
|
+
$stderr.puts "Discarding it - we've received registry sync at <#{@earliest_tick}>"
|
109
|
+
return
|
110
|
+
end
|
111
|
+
# Invalidate old spaces that were generated without this information
|
112
|
+
at_tick.upto(@tick) {|old_tick| @spaces.delete old_tick}
|
113
|
+
end
|
114
|
+
@deltas[at_tick] << delta
|
115
|
+
end
|
116
|
+
|
117
|
+
def apply_deltas_before_update(at_tick)
|
118
|
+
space = space_at(at_tick)
|
119
|
+
|
120
|
+
@deltas[at_tick].each do |hash|
|
121
|
+
players = hash[:add_players]
|
122
|
+
add_players(space, players) if players
|
123
|
+
|
124
|
+
doomed = hash[:delete_entities]
|
125
|
+
delete_entities(space, doomed) if doomed
|
126
|
+
|
127
|
+
updated = hash[:update_entities]
|
128
|
+
update_entities(space, updated) if updated
|
129
|
+
|
130
|
+
move = hash[:move]
|
131
|
+
apply_move(space, move) if move
|
132
|
+
|
133
|
+
score_update = hash[:update_score]
|
134
|
+
update_score(space, score_update) if score_update
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def apply_deltas_after_update(at_tick)
|
139
|
+
space = space_at(at_tick)
|
140
|
+
|
141
|
+
@deltas[at_tick].each do |hash|
|
142
|
+
npcs = hash[:add_npcs]
|
143
|
+
add_npcs(space, npcs) if npcs
|
144
|
+
end
|
145
|
+
|
146
|
+
# Any later spaces are now invalid
|
147
|
+
@spaces.delete_if {|key, _| key > at_tick}
|
148
|
+
end
|
149
|
+
|
150
|
+
def apply_all_deltas(at_tick)
|
151
|
+
apply_deltas_before_update(at_tick)
|
152
|
+
apply_deltas_after_update(at_tick)
|
153
|
+
end
|
154
|
+
|
155
|
+
def add_player(space, hash)
|
156
|
+
player = Serializable.from_json(hash)
|
157
|
+
puts "Added player #{player}"
|
158
|
+
space << player
|
159
|
+
player.registry_id
|
160
|
+
end
|
161
|
+
|
162
|
+
def add_players(space, players)
|
163
|
+
players.each {|json| add_player(space, json) }
|
164
|
+
end
|
165
|
+
|
166
|
+
def apply_move(space, move)
|
167
|
+
player_id = move[:player_id]
|
168
|
+
player = space[player_id]
|
169
|
+
fail "No such player #{player_id}, can't apply #{move.inspect}" unless player
|
170
|
+
player.add_move move
|
171
|
+
end
|
172
|
+
|
173
|
+
def add_npcs(space, npcs)
|
174
|
+
npcs.each {|json| space << Serializable.from_json(json) }
|
175
|
+
end
|
176
|
+
|
177
|
+
def add_entity(space, json)
|
178
|
+
space << Serializable.from_json(json)
|
179
|
+
end
|
180
|
+
|
181
|
+
# Returns the set of registry IDs updated or added
|
182
|
+
def update_entities(space, updated)
|
183
|
+
registry_ids = Set.new
|
184
|
+
updated.each do |json|
|
185
|
+
registry_id = json[:registry_id]
|
186
|
+
fail "Can't update #{entity.inspect}, no registry_id!" unless registry_id
|
187
|
+
registry_ids << registry_id
|
188
|
+
|
189
|
+
if my_obj = space[registry_id]
|
190
|
+
my_obj.update_from_json(json)
|
191
|
+
else
|
192
|
+
add_entity(space, json)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
registry_ids
|
197
|
+
end
|
198
|
+
|
199
|
+
def delete_entities(space, doomed)
|
200
|
+
doomed.each do |registry_id|
|
201
|
+
dead = space[registry_id]
|
202
|
+
next unless dead
|
203
|
+
puts "Disconnected: #{dead}" if dead.is_a? Player
|
204
|
+
space.doom dead
|
205
|
+
end
|
206
|
+
space.purge_doomed_entities
|
207
|
+
end
|
208
|
+
|
209
|
+
def update_score(space, update)
|
210
|
+
registry_id, score = update.to_a.first
|
211
|
+
return unless player = space[registry_id]
|
212
|
+
player.score = score
|
213
|
+
end
|
214
|
+
|
215
|
+
# Discard anything we think we know, in favor of the registry
|
216
|
+
# we just got from the server
|
217
|
+
def sync_registry(server_registry, highest_id, at_tick)
|
218
|
+
@spaces.clear
|
219
|
+
# Any older deltas are now irrelevant
|
220
|
+
@earliest_tick.upto(at_tick - 1) {|old_tick| @deltas.delete old_tick}
|
221
|
+
update_entities(create_initial_space(at_tick, highest_id), server_registry)
|
222
|
+
|
223
|
+
# The server has given us a complete, finished frame. Don't
|
224
|
+
# create a new one until this one has been displayed once.
|
225
|
+
@preprocessed = at_tick
|
226
|
+
end
|
227
|
+
end
|