game_2d 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -2,14 +2,37 @@
2
2
 
3
3
  A 2D sandbox game, using Gosu and REnet.
4
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:
5
+ ## Overview
6
+
7
+ Built on top of Gosu, an engine for making 2-D games. Gosu provides the means
8
+ to handle the graphics, sound, and keyboard/mouse events. It doesn't provide
9
+ any sort of client/server network architecture for multiplayer games, nor a
10
+ system for tracking objects in game-space. This gem aims to fill that gap.
11
+
12
+ Originally I tried using Chipmunk as the physics engine, but its outcomes were
13
+ too unpredictable for the client to anticipate the server. It was also hard to
14
+ constrain in the ways I wanted. So I elected to build something integer-based.
15
+
16
+ In the short term, I'm throwing anything into this gem that interests me. There
17
+ are reusable elements (GameSpace, Entity, ServerPort), and game-specific
18
+ elements (particular Entity subclasses with custom behaviors). Longer term, I
19
+ could see splitting it into two gems. This gem, game_2d, would retain the
20
+ reusable platform classes. The other classes would move into a new gem specific
21
+ to the game I'm developing, as a sort of reference implementation.
22
+
23
+ ## Design
24
+
25
+ ### GameSpace
6
26
 
7
- * The server runs at 60 frames a second, to match Gosu.
8
27
  * A GameSpace holds a copy of the game's state. This is primarily a grid populated by Entities.
9
28
  * The dimensions of the GameSpace (in cells) are specified on server startup.
10
29
  * 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
30
  * 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
31
  * New Entities are assigned IDs in order, again, to allow the client to predict which ID will be assigned to each new entity.
32
+
33
+ ### Client/Server
34
+
35
+ * The server runs at 60 frames a second, to match Gosu.
13
36
  * Clients connect using REnet. The handshaking process creates a Player object to represent the client.
14
37
  * Client and server use Diffie-Hellman key exchange to establish a shared secret. This is used to encrypt sensitive information, rather like SSH (but over REnet).
15
38
  * Right now, the only sensitive information is the user's password. This allows the server to remember who's who.
@@ -18,23 +41,7 @@ There's not much here yet, in terms of actual gameplay. At this point I'm layin
18
41
  * Nothing else interesting here yet, but I fully expect to put an inventory here, once there is anything useful to carry around.
19
42
  * This is also where I plan to put authorization data - who's a god player (level editor) and who isn't.
20
43
  * Player data is persisted in ~/.game_2d/players/players as JSON text.
21
- * Keyboard commands control the player:
22
- * A and D, or LeftArrow and RightArrow: Hold down to accelerate.
23
- * LeftControl or RightControl: Hold down to brake (slow down).
24
- * S, or DownArrow: Hold down to build (the longer you hold, the stronger the block).
25
- * W, or UpArrow:
26
- * When not building: Flip (turn 180 degrees).
27
- * When building: Rise up (ascend to the top of the built block).
28
- * Left button: Fire a pellet. Mouse click determines the direction and speed -- closer to the player means slower speed.
29
- * Right button: Grab an entity and drag it around. Right-click again to release. Grab is affected by snap-to-grid. *(This will eventually become part of a level-editing mode.)*
30
- * Shifted right button, or B: Build a block where the mouse points. *(This will eventually become part of a level-editing mode.)*
31
- * 1 through 7: Shortcuts for building Dirt, Brick, Cement, Steel, Unlikelium, Titanium, Teleporter.
32
- * Mousing over an entity displays some basic information about it. *(This will eventually become part of a level-editing mode.)*
33
- * A simple menu system is also available. Left-click to select. Esc returns to the top-level.
34
- * The menus include a Save feature, which tells the server to save the level's state.
35
- * Levels are persisted in ~/.game_2d/levels/<level name> as JSON text.
36
- * On subsequent startup with the same name, the level is loaded.
37
- * The client and server communicate using JSON text. This is undoubtedly inefficient, but aids in debugging at this early stage.
44
+ * The client and server communicate using JSON text. This is undoubtedly inefficient, but aids in debugging at this early stage. I fully intend to replace this, eventually.
38
45
  * Every frame is numbered by the server, and is referred to as a 'tick'.
39
46
  * Player actions are both executed locally, and sent to the server.
40
47
  * If other players are connected, the server sends everyone's actions to everyone else.
@@ -45,7 +52,50 @@ There's not much here yet, in terms of actual gameplay. At this point I'm layin
45
52
  * If the server sends something conflicting, the client discards its wrong prediction.
46
53
  * This is intended to compensate for dropped packets or lag, but much more testing is needed.
47
54
 
48
- 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:
55
+ ### Player Interface
56
+
57
+ * Keyboard commands control the player object, which is called a Gecko:
58
+ * A and D, or LeftArrow and RightArrow: Slide sideways. Hold down to accelerate.
59
+ * LeftControl or RightControl: Hold down to brake (slow down).
60
+ * S, or DownArrow: Hold down to build (the longer you hold, the stronger the block).
61
+ * W, or UpArrow:
62
+ * When not building: Flip (turn 180 degrees).
63
+ * When building: Rise up (ascend to the top of the built block).
64
+ * The mouse is also used.
65
+ * Left button: Fire a pellet. Mouse click determines the direction and speed.
66
+ * When firing up, the pellet's trajectory will peak where you clicked.
67
+ * When firing down, the pellet will be fired horizontally such that it falls through the place where you clicked.
68
+ * A simple menu system is available. Left-click to select. Esc returns to the top-level.
69
+
70
+ ### Level-Editing Features
71
+
72
+ This will eventually become a separate game mode, accessible only to certain authorized players.
73
+
74
+ * Menu options let you select the type of entity to build, and turn "snap to grid" on or off.
75
+ * Snap-to-grid aims to put entities exactly in particular cells, with X and Y coordinates both multiples of 400.
76
+ * Right button: Grab an entity and drag it around. Right-click again to release. Grab is affected by snap-to-grid.
77
+ * Shifted right button, or B: Build a block where the mouse points, of the type selected in the menu.
78
+ * Shortcut keys for particular object types:
79
+ * 1: Dirt
80
+ * 2: Brick
81
+ * 3: Cement
82
+ * 4: Steel
83
+ * 5: Unlikelium
84
+ * 6: Titanium
85
+ * 7: Teleporter
86
+ * 8: Hole
87
+ * 9: Base
88
+ * 0: Slime
89
+ * Mousing over an entity displays some basic information about it.
90
+ * The menus include a Save feature, which tells the server to save the level's state.
91
+ * Levels are persisted in ~/.game_2d/levels/<level name> as JSON text.
92
+ * On subsequent startup with the same name, the level is loaded.
93
+
94
+ ## Gameplay
95
+
96
+ ### Block physics
97
+
98
+ The physics is (intentionally) pretty simple. Unsupported blocks 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:
49
99
 
50
100
  * Dirt blocks (1-5 HP) fall unless something is underneath.
51
101
  * Brick blocks (6-10 HP) can stay up if supported from both left and right.
@@ -53,21 +103,51 @@ The physics is (intentionally) pretty simple. Unsupported objects fall, acceler
53
103
  * Steel blocks (16-20 HP) can stay up if touching anything else, even above.
54
104
  * Unlikelium blocks (21-25 HP) never fall.
55
105
 
106
+ Brick and Cement blocks are considered supported from the sides only if the entities present there are at exactly the same height. Steel blocks are less picky; they only fall if nothing is touching them.
107
+
56
108
  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.
57
109
 
58
110
  Whether a block is supported depends exclusively on its immediate surroundings. That means two blocks 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.
59
111
 
60
- 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. So far, only blocks can take damage; players are indestructible (but you know that will change).
112
+ There are also Titanium entities. They take up space and can be used for support, but they are not truly blocks. They never fall, and are indestructible. These are intended for use in designing levels with specific shapes. They can only be created, destroyed, or moved by using the level-editing features.
61
113
 
62
- 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 shift-right-clicking.
114
+ The GameSpace is bounded by invisible, indestructible Wall entities. These are like Titanium, except that they are off-screen, and cannot be altered even with the level-editing features.
63
115
 
64
- Teleporters never fall. Their only action is to transfer their contents (anything close enough to intersect with their center) to their single destination-point. This transfer affects location but not velocity. Transfer doesn't happen if there is something blocking the exit point.
116
+ ### Geckos
65
117
 
66
- 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).
118
+ A player object is called a Gecko, because of how it moves. A Gecko is considered to be supported if its "feet" are touching a block. Unsupported Geckos will turn feet-downward and fall, until they land on something. Supported Geckos 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 Gecko's head is exactly touching another block. At all other times, a flip leads to a fall (which can be useful too).
67
119
 
68
120
  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.
69
121
 
70
- The GameSpace is bounded by invisible, indestructible Wall entities. These can also support blocks, or the player.
122
+ ### Bases
123
+
124
+ A base is a spawn point for players, and is also an object that can be moved. When a game server is started with a new level name, a single starter base will be created in the center (and then promptly fall to the bottom). New bases can be created using the level-editing features, as many as desired, and placed wherever it makes sense for players to enter the level. Bases are indestructible (currently).
125
+
126
+ Like Geckos, Bases can perch on walls or ceilings, as long as their "feet" are pointed the right way. Unlike Geckos, when a Base gets thrown sideways or upwards, it will turn its "feet" in the direction it's going. So it will tend to stick to the first thing it hits.
127
+
128
+ Bases are physical objects, and most other objects (like blocks) can't move through them; but they're transparent to players. Players who join the game will start out at a randomly selected unoccupied base, in the same orientation as the base.
129
+
130
+ ## Ghosts
131
+
132
+ When a Gecko is destroyed, the player turns into a Ghost. This means you're dead. A Ghost can't touch anything or affect anything, and isn't affected by anything, even gravity. Ghosts can only float around, and look at things.
133
+
134
+ One other thing a Ghost can do is turn back into a Gecko, i.e. respawn. The player gets some choice as to which base to respawn at, if more than one exist and are unoccupied. The Ghost will move quickly to the unoccupied base nearest to the click position (even if that's really far away). If the player's not choosy, they may click anywhere. If all bases are occupied, nothing happens; the Ghost player must wait for those other Geckos to get out of the way.
135
+
136
+ This is also the solution for allowing new players to enter the game when all bases are occupied: The new player is created as a Ghost.
137
+
138
+ ### Pellets
139
+
140
+ 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. Blocks, slime, and players can take damage and be destroyed.
141
+
142
+ ### Teleporters
143
+
144
+ Teleporters never fall, and they are "transparent" to most entities; they act as if part of the background. Their only action is to transfer their contents (anything close enough to intersect with their center) to their single destination-point. This transfer affects location but not velocity. Transfer doesn't happen if there is something blocking the exit point.
145
+
146
+ ### Holes
147
+
148
+ They're black. Really black. Against the starry background, they're impossible to see... except that they occlude the stars.
149
+
150
+ Mostly, holes are detectable just like real black holes: by virtue of the effect they have on everything around them. Normal gravity makes loose objects fall downward, but in the vicinity of a hole, gravity sucks all objects toward the hole. If the falling object gets too close to the hole, it will begin taking damage. Blocks will rapidly be destroyed as they approach. Pellets can't take damage in this way, but their path will bend around the hole. Pellets can even be captured in orbit around the hole, though they always escape eventually.
71
151
 
72
152
 
73
153
  ## Installation
@@ -88,6 +168,12 @@ And a client in another window:
88
168
 
89
169
  $ game_2d_client.rb --name Bender --hostname 127.0.0.1
90
170
 
171
+ The client can use the menu to select "Save", telling the server to save a copy of the level. Subsequent runs of the server can leave off the dimensions:
172
+
173
+ $ game_2d_server.rb --level example
174
+
175
+ Run these commands with --help to see more options.
176
+
91
177
  ## Contributing
92
178
 
93
179
  1. Fork it ( https://github.com/sereneiconoclast/game_2d/fork )
@@ -4,29 +4,45 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
  require 'game_2d/version'
5
5
 
6
6
  Gem::Specification.new do |spec|
7
- spec.name = "game_2d"
7
+ spec.name = 'game_2d'
8
8
  spec.version = Game2d::VERSION
9
- spec.authors = ["Greg Meyers"]
10
- spec.email = ["cmdr.samvimes@gmail.com"]
9
+ spec.authors = ['Greg Meyers']
10
+ spec.email = ['cmdr.samvimes@gmail.com']
11
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"
12
+ spec.description = <<EOF
13
+ Built on top of Gosu, an engine for making 2-D games. Gosu provides the means
14
+ to handle the graphics, sound, and keyboard/mouse events. It doesn't provide
15
+ any sort of client/server network architecture for multiplayer games, nor a
16
+ system for tracking objects in game-space. This gem aims to fill that gap.
17
+
18
+ Originally I tried using Chipmunk as the physics engine, but its outcomes were
19
+ too unpredictable for the client to anticipate the server. It was also hard to
20
+ constrain in the ways I wanted. So I elected to build something integer-based.
21
+
22
+ In the short term, I'm throwing anything into this gem that interests me. There
23
+ are reusable elements (GameSpace, Entity, ServerPort), and game-specific
24
+ elements (particular Entity subclasses with custom behaviors). Longer term, I
25
+ could see splitting it into two gems. This gem, game_2d, would retain the
26
+ reusable platform classes. The other classes would move into a new gem specific
27
+ to the game I'm developing, as a sort of reference implementation.
28
+ EOF
29
+ spec.homepage = 'https://github.com/sereneiconoclast/game_2d'
30
+ spec.license = 'MIT'
15
31
 
16
32
  spec.files = `git ls-files -z`.split("\x0")
17
33
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
34
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
- spec.require_paths = ["lib"]
20
- spec.required_ruby_version = ">= 1.9.3"
35
+ spec.require_paths = ['lib']
36
+ spec.required_ruby_version = '>= 1.9.3'
21
37
 
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"]
38
+ spec.add_runtime_dependency 'facets', ['>= 2.9.3']
39
+ spec.add_runtime_dependency 'gosu', ['>= 0.8.5']
40
+ spec.add_runtime_dependency 'json', ['>= 1.8.1']
41
+ spec.add_runtime_dependency 'renet', ['>= 0.1.14']
42
+ spec.add_runtime_dependency 'trollop', ['>= 2.0']
27
43
 
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"
44
+ spec.add_development_dependency 'bundler', '~> 1.7'
45
+ spec.add_development_dependency 'rake', '~> 10.0'
46
+ spec.add_development_dependency 'rr', '~> 1.1.2'
47
+ spec.add_development_dependency 'rspec', '~> 3.1.0'
32
48
  end
@@ -108,9 +108,9 @@ class ClientConnection
108
108
  if you_are
109
109
  # The 'world' response includes deltas for add_players and add_npcs
110
110
  # Need to process those first, as one of the players is us
111
- @engine.apply_deltas(at_tick)
111
+ @engine.apply_deltas(at_tick) if world
112
112
 
113
- @engine.create_local_player you_are
113
+ @game.player_id = you_are
114
114
  end
115
115
 
116
116
  @engine.sync_registry(registry, highest_id, at_tick) if registry
@@ -120,12 +120,12 @@ class ClientConnection
120
120
  @engine.tick + ACTION_DELAY
121
121
  end
122
122
 
123
- def send_move(move, args={})
123
+ def send_move(player_id, move, args={})
124
124
  return unless move && online?
125
125
  args[:move] = move.to_s
126
126
  delta = { :at_tick => send_actions_at, :move => args }
127
127
  send_record delta
128
- delta[:player_id] = @engine.player_id
128
+ delta[:player_id] = player_id
129
129
  @engine.add_delta delta
130
130
  end
131
131
 
@@ -144,6 +144,13 @@ class ClientConnection
144
144
  @engine.add_delta delta
145
145
  end
146
146
 
147
+ def send_delete_entity(entity)
148
+ return unless online?
149
+ delta = { :delete_entities => [entity.registry_id], :at_tick => send_actions_at }
150
+ send_record delta
151
+ @engine.add_delta delta
152
+ end
153
+
147
154
  def send_snap_to_grid(entity)
148
155
  return unless online? && entity
149
156
  delta = { :at_tick => send_actions_at, :snap_to_grid => entity.registry_id }
@@ -61,7 +61,6 @@ class ClientEngine
61
61
 
62
62
  last_space = space_at(tick - 1)
63
63
  @spaces[tick] = new_space = GameSpace.new(@game_window).copy_from(last_space)
64
-
65
64
  apply_deltas(tick)
66
65
  new_space.update
67
66
 
@@ -78,7 +77,7 @@ class ClientEngine
78
77
  end
79
78
 
80
79
  if @tick - @earliest_tick >= MAX_LEAD_TICKS
81
- $stderr.puts "Lost connection? Running ahead of server?"
80
+ warn "Lost connection? Running ahead of server?"
82
81
  return space_at(@tick)
83
82
  end
84
83
  space_at(@tick += 1)
@@ -88,25 +87,13 @@ class ClientEngine
88
87
  @spaces[@tick]
89
88
  end
90
89
 
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
90
  def add_delta(delta)
104
- at_tick = delta.delete :at_tick
91
+ at_tick = delta[:at_tick]
105
92
  fail "Received delta without at_tick: #{delta.inspect}" unless at_tick
106
93
  if at_tick < @tick
107
- $stderr.puts "Received delta #{@tick - at_tick} ticks late"
94
+ warn "Received delta #{@tick - at_tick} ticks late"
108
95
  if at_tick <= @earliest_tick
109
- $stderr.puts "Discarding it - we've received registry sync at <#{@earliest_tick}>"
96
+ warn "Discarding it - we've received registry sync at <#{@earliest_tick}>"
110
97
  return
111
98
  end
112
99
  # Invalidate old spaces that were generated without this information
@@ -135,6 +122,7 @@ class ClientEngine
135
122
  add_npcs(space, npcs) if npcs
136
123
 
137
124
  move = hash.delete :move
125
+ player_name = hash.delete :player_name
138
126
  player_id = hash.delete :player_id
139
127
  if move
140
128
  fail "No player_id sent with move #{move.inspect}" unless player_id
@@ -144,7 +132,8 @@ class ClientEngine
144
132
  score_update = hash.delete :update_score
145
133
  update_score(space, score_update) if score_update
146
134
 
147
- $stderr.puts "Unprocessed deltas: #{hash.keys.join(', ')}" unless hash.empty?
135
+ leftovers = hash.keys - [:at_tick]
136
+ warn "Unprocessed deltas: #{leftovers.join(', ')}" unless leftovers.empty?
148
137
  end
149
138
  end
150
139
 
@@ -161,8 +150,13 @@ class ClientEngine
161
150
 
162
151
  def apply_move(space, move, player_id)
163
152
  player = space[player_id]
164
- fail "No such player #{player_id}, can't apply #{move.inspect}" unless player
165
- player.add_move move
153
+ if player
154
+ player.add_move move
155
+ else
156
+ # This can happen if, say, the player sent an action just before
157
+ # death or disconnection.
158
+ warn "No such player #{player_id}, can't apply #{move.inspect}"
159
+ end
166
160
  end
167
161
 
168
162
  def add_npcs(space, npcs)
@@ -174,7 +168,15 @@ class ClientEngine
174
168
  end
175
169
 
176
170
  def add_entity(space, json)
177
- space << Serializable.from_json(json)
171
+ space.add_entity (o = Serializable.from_json(json))
172
+ if o.is_a?(Player) && @game_window.player_name == o.player_name
173
+ # This can be news, if the server is ahead of us. The server
174
+ # promises that we always have exactly one entity assigned to
175
+ # each authenticated player, and each connected player must
176
+ # have a unique name at any time -- so this entity has to be
177
+ # ours.
178
+ @game_window.player_id = o.registry_id
179
+ end
178
180
  end
179
181
 
180
182
  # Returns the set of registry IDs updated or added
@@ -30,8 +30,8 @@ class Entity
30
30
  def bottom_cell_y_at(y); (y + HEIGHT - 1) / HEIGHT; end
31
31
 
32
32
  # Velocity is constrained to the range -MAX_VELOCITY .. MAX_VELOCITY
33
- def constrain_velocity(vel)
34
- [[vel, MAX_VELOCITY].min, -MAX_VELOCITY].max
33
+ def constrain_velocity(vel, max=MAX_VELOCITY)
34
+ [[vel, max].min, -max].max
35
35
  end
36
36
  end
37
37
  include ClassMethods
@@ -57,7 +57,7 @@ class Entity
57
57
  @grabbed = false
58
58
  end
59
59
 
60
- def a=(angle); @a = angle % 360; end
60
+ def a=(angle); @a = (angle || 0) % 360; end
61
61
 
62
62
  def x_vel=(xv)
63
63
  @x_vel = constrain_velocity xv
@@ -66,6 +66,9 @@ class Entity
66
66
  @y_vel = constrain_velocity yv
67
67
  end
68
68
 
69
+ def cx; x + WIDTH/2; end
70
+ def cy; y + HEIGHT/2; end
71
+
69
72
  # True if we need to update this entity
70
73
  def moving?; @moving; end
71
74
 
@@ -115,9 +118,9 @@ class Entity
115
118
  end
116
119
 
117
120
  # Apply acceleration
118
- def accelerate(x_accel, y_accel)
119
- self.x_vel = @x_vel + x_accel
120
- self.y_vel = @y_vel + y_accel
121
+ def accelerate(x_accel, y_accel, max=MAX_VELOCITY)
122
+ @x_vel = constrain_velocity(@x_vel + x_accel, max) if x_accel
123
+ @y_vel = constrain_velocity(@y_vel + y_accel, max) if y_accel
121
124
  end
122
125
 
123
126
  def opaque(others)
@@ -132,6 +135,20 @@ class Entity
132
135
  opaque(@space.entities_overlapping(new_x, new_y))
133
136
  end
134
137
 
138
+ def slow_by(amount)
139
+ if @x_vel.zero?
140
+ self.y_vel = slower_speed(@y_vel, amount)
141
+ else
142
+ self.x_vel = slower_speed(@x_vel, amount)
143
+ end
144
+ end
145
+
146
+ def slower_speed(current, delta)
147
+ return 0 if current.abs < delta
148
+ sign = current <=> 0
149
+ sign * (current.abs - delta)
150
+ end
151
+
135
152
  # Process one tick of motion, horizontally only
136
153
  def move_x
137
154
  return if doomed?
@@ -153,8 +170,8 @@ class Entity
153
170
  impacts.delete_if {|e| e.x < impact_at_x }
154
171
  impact_at_x + WIDTH
155
172
  end
173
+ i_hit(impacts, @x_vel.abs)
156
174
  self.x_vel = 0
157
- i_hit(impacts)
158
175
  end
159
176
 
160
177
  # Process one tick of motion, vertically only
@@ -178,8 +195,8 @@ class Entity
178
195
  impacts.delete_if {|e| e.y < impact_at_y }
179
196
  impact_at_y + HEIGHT
180
197
  end
198
+ i_hit(impacts, @y_vel.abs)
181
199
  self.y_vel = 0
182
- i_hit(impacts)
183
200
  end
184
201
 
185
202
  # Process one tick of motion. Only called when moving? is true
@@ -202,7 +219,7 @@ class Entity
202
219
  # Handle any behavior specific to this entity
203
220
  # Default: Accelerate downward if the subclass says we should fall
204
221
  def update
205
- accelerate(0, 1) if should_fall?
222
+ space.fall(self) if should_fall?
206
223
  move
207
224
  end
208
225
 
@@ -220,12 +237,17 @@ class Entity
220
237
  end
221
238
  end
222
239
 
223
- def i_hit(other)
224
- # TODO
225
- puts "#{self} hit #{other.inspect}"
240
+ # Most entities can be teleported, but not when grabbed
241
+ def teleportable?
242
+ !grabbed?
226
243
  end
227
244
 
228
- def harmed_by(other); end
245
+ # 'others' is an array of impacted entities
246
+ # 'velocity' is the absolute value of the x_vel or y_vel
247
+ # that was being applied when the hit occurred
248
+ def i_hit(others, velocity); end
249
+
250
+ def harmed_by(other, damage=1); end
229
251
 
230
252
  # Return any entities adjacent to this one in the specified direction
231
253
  def next_to(angle, x=@x, y=@y)
@@ -243,10 +265,18 @@ class Entity
243
265
  @space.entities_at_points(points)
244
266
  end
245
267
 
246
- def empty_underneath?; opaque(next_to(180)).empty?; end
247
- def empty_on_left?; opaque(next_to(270)).empty?; end
248
- def empty_on_right?; opaque(next_to(90)).empty?; end
249
- def empty_above?; opaque(next_to(0)).empty?; end
268
+ def underfoot
269
+ opaque(next_to(self.a + 180))
270
+ end
271
+
272
+ def beneath; opaque(next_to(180)); end
273
+ def on_left; opaque(next_to(270)); end
274
+ def on_right; opaque(next_to(90)); end
275
+ def above; opaque(next_to(0)); end
276
+ def empty_underneath?; beneath.empty?; end
277
+ def empty_on_left?; on_left.empty?; end
278
+ def empty_on_right?; on_right.empty?; end
279
+ def empty_above?; above.empty?; end
250
280
 
251
281
  def angle_to_vector(angle, amplitude=1)
252
282
  case angle % 360
@@ -276,10 +306,16 @@ class Entity
276
306
 
277
307
  # Given a vector with a diagonal, drop the smaller component, returning a
278
308
  # vector that is strictly either horizontal or vertical.
279
- def drop_diagonal(x_vel, y_vel)
309
+ def drop_diagonal(x_vel=@x_vel, y_vel=@y_vel)
280
310
  (y_vel.abs > x_vel.abs) ? [0, y_vel] : [x_vel, 0]
281
311
  end
282
312
 
313
+ # Roughly speaking, are we going left, right, up, or down?
314
+ def direction
315
+ return nil if x_vel.zero? && y_vel.zero?
316
+ vector_to_angle(*drop_diagonal)
317
+ end
318
+
283
319
  # Is the other entity basically above us, below us, or on the left or the
284
320
  # right? Returns the angle we should face if we want to face that entity.
285
321
  def direction_to(other_x, other_y)
@@ -333,6 +369,45 @@ class Entity
333
369
  end
334
370
  end
335
371
 
372
+ # Apply a move where this entity slides past another
373
+ # If it reaches the other entity's corner, it will turn at
374
+ # right angles to go around that corner
375
+ #
376
+ # apply_turn: true if this entity's angle should be adjusted
377
+ # during the turn
378
+ #
379
+ # Returns true if a corner was reached and we went around it,
380
+ # false if that didn't happen (in which case, no move occurred)
381
+ def slide_around(other, apply_turn = true)
382
+ # Figure out where corner is and whether we're about to reach or pass it
383
+ corner, distance, overshoot, turn = going_past_entity(other.x, other.y)
384
+ return false unless corner
385
+
386
+ original_speed = @x_vel.abs + @y_vel.abs
387
+ original_dir = vector_to_angle
388
+ new_dir = original_dir + turn
389
+
390
+ # Make sure nothing occupies any space we're about to move through
391
+ return false unless opaque(
392
+ @space.entities_overlapping(*corner) + next_to(new_dir, *corner)
393
+ ).empty?
394
+
395
+ # Move to the corner
396
+ self.x_vel, self.y_vel = angle_to_vector(original_dir, distance)
397
+ move
398
+
399
+ # Turn and apply remaining velocity
400
+ # Make sure we move at least one subpixel so we don't sit exactly at
401
+ # the corner, and fall
402
+ self.a += turn if apply_turn
403
+ overshoot = 1 if overshoot.zero?
404
+ self.x_vel, self.y_vel = angle_to_vector(new_dir, overshoot)
405
+ move
406
+
407
+ self.x_vel, self.y_vel = angle_to_vector(new_dir, original_speed)
408
+ true
409
+ end
410
+
336
411
  def as_json
337
412
  Serializable.as_json(self).merge!(
338
413
  :class => self.class.to_s,
@@ -360,9 +435,17 @@ class Entity
360
435
 
361
436
  def draw_zorder; ZOrder::Objects end
362
437
 
438
+ def draw_animation(window)
439
+ window.animation[window.media(image_filename)]
440
+ end
441
+
442
+ def draw_image(anim)
443
+ anim[Gosu::milliseconds / 100 % anim.size]
444
+ end
445
+
363
446
  def draw(window)
364
- anim = window.animation[window.media(image_filename)]
365
- img = anim[Gosu::milliseconds / 100 % anim.size]
447
+ img = draw_image(draw_animation(window))
448
+
366
449
  # Entity's pixel_x/pixel_y is the location of the upper-left corner
367
450
  # draw_rot wants us to specify the point around which rotation occurs
368
451
  # That should be the center