Class: Entity

Inherits:
Object
  • Object
show all
Includes:
EntityConstants, Registerable, Serializable
Defined in:
lib/game_2d/entity.rb,
lib/game_2d/entity/block.rb,
lib/game_2d/entity/pellet.rb,
lib/game_2d/entity/titanium.rb,
lib/game_2d/entity/owned_entity.rb

Direct Known Subclasses

OwnedEntity, Titanium, Player, Wall

Defined Under Namespace

Classes: Block, OwnedEntity, Pellet, Titanium

Constant Summary

Constants included from EntityConstants

EntityConstants::CELL_WIDTH_IN_PIXELS, EntityConstants::MAX_VELOCITY, EntityConstants::PIXEL_WIDTH, EntityConstants::WIDTH

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Registerable

#nullsafe_registry_id, #registry_id, #registry_id=, #registry_id?, #registry_id_safe

Methods included from Serializable

#<=>, #==, as_json, #eql?, from_json, #hash, #to_json

Constructor Details

#initialize(x = 0, y = 0, a = 0, x_vel = 0, y_vel = 0) ⇒ Entity

space: the game space x, y: position in sub-pixels of the upper-left corner a: angle, with 0 = up, 90 = right x_vel, y_vel: velocity in sub-pixels


26
27
28
29
30
# File 'lib/game_2d/entity.rb', line 26

def initialize(x = 0, y = 0, a = 0, x_vel = 0, y_vel = 0)
  @x, @y, self.a = x, y, a
  self.x_vel, self.y_vel = x_vel, y_vel
  @moving = true
end

Instance Attribute Details

#aObject

Returns the value of attribute a.


20
21
22
# File 'lib/game_2d/entity.rb', line 20

def a
  @a
end

#movingObject

X and Y position of the top-left corner


18
19
20
# File 'lib/game_2d/entity.rb', line 18

def moving
  @moving
end

#spaceObject

X and Y position of the top-left corner


18
19
20
# File 'lib/game_2d/entity.rb', line 18

def space
  @space
end

#xObject

X and Y position of the top-left corner


18
19
20
# File 'lib/game_2d/entity.rb', line 18

def x
  @x
end

#x_velObject

Returns the value of attribute x_vel.


20
21
22
# File 'lib/game_2d/entity.rb', line 20

def x_vel
  @x_vel
end

#yObject

X and Y position of the top-left corner


18
19
20
# File 'lib/game_2d/entity.rb', line 18

def y
  @y
end

#y_velObject

Returns the value of attribute y_vel.


20
21
22
# File 'lib/game_2d/entity.rb', line 20

def y_vel
  @y_vel
end

Class Method Details

.bottom_cell_y_at(y) ⇒ Object

Bottom-most cell Y position occupied


87
# File 'lib/game_2d/entity.rb', line 87

def self.bottom_cell_y_at(y); (y + HEIGHT - 1) / HEIGHT; end

.left_cell_x_at(x) ⇒ Object

Left-most cell X position occupied


72
# File 'lib/game_2d/entity.rb', line 72

def self.left_cell_x_at(x); x / WIDTH; end

.right_cell_x_at(x) ⇒ Object

Right-most cell X position occupied If we’re exactly within a column (@x is an exact multiple of WIDTH), then this equals left_cell_x. Otherwise, it’s one higher


79
# File 'lib/game_2d/entity.rb', line 79

def self.right_cell_x_at(x); (x + WIDTH - 1) / WIDTH; end

.top_cell_y_at(y) ⇒ Object

Top-most cell Y position occupied


83
# File 'lib/game_2d/entity.rb', line 83

def self.top_cell_y_at(y); y / HEIGHT; end

Instance Method Details

#accelerate(x_accel, y_accel) ⇒ Object

Apply acceleration


99
100
101
102
# File 'lib/game_2d/entity.rb', line 99

def accelerate(x_accel, y_accel)
  self.x_vel = @x_vel + x_accel
  self.y_vel = @y_vel + y_accel
end

#all_stateObject


368
369
370
# File 'lib/game_2d/entity.rb', line 368

def all_state
  [registry_id_safe, @x, @y, @a, @x_vel, @y_vel, @moving]
end

#angle_to_vector(angle, amplitude = 1) ⇒ Object


236
237
238
239
240
241
242
243
244
# File 'lib/game_2d/entity.rb', line 236

def angle_to_vector(angle, amplitude=1)
  case angle % 360
  when 0 then [0, -amplitude]
  when 90 then [amplitude, 0]
  when 180 then [0, amplitude]
  when 270 then [-amplitude, 0]
  else raise "Trig unimplemented"
  end
end

#as_jsonObject


321
322
323
324
325
326
327
328
329
330
# File 'lib/game_2d/entity.rb', line 321

def as_json
  Serializable.as_json(self).merge!(
    :class => self.class.to_s,
    :registry_id => registry_id,
    :position => [ self.x, self.y ],
    :velocity => [ self.x_vel, self.y_vel ],
    :angle => self.a,
    :moving => self.moving?
  )
end

#bottom_cell_y(y = @y) ⇒ Object


88
# File 'lib/game_2d/entity.rb', line 88

def bottom_cell_y(y = @y); self.class.bottom_cell_y_at(y); end

#destroy!Object

Give this entity a chance to perform clean-up upon destruction


64
# File 'lib/game_2d/entity.rb', line 64

def destroy!; end

#direction_to(other_x, other_y) ⇒ Object

Is the other entity basically above us, below us, or on the left or the right? Returns the angle we should face if we want to face that entity.


270
271
272
# File 'lib/game_2d/entity.rb', line 270

def direction_to(other_x, other_y)
  vector_to_angle(*drop_diagonal(other_x - @x, other_y - @y))
end

#doomed?Boolean

Returns:

  • (Boolean)

45
# File 'lib/game_2d/entity.rb', line 45

def doomed?; @space.doomed?(self); end

#draw(window) ⇒ Object


348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
# File 'lib/game_2d/entity.rb', line 348

def draw(window)
  anim = window.animation[window.media(image_filename)]
  img = anim[Gosu::milliseconds / 100 % anim.size]
  # Entity's pixel_x/pixel_y is the location of the upper-left corner
  # draw_rot wants us to specify the point around which rotation occurs
  # That should be the center
  img.draw_rot(
    self.pixel_x + CELL_WIDTH_IN_PIXELS / 2,
    self.pixel_y + CELL_WIDTH_IN_PIXELS / 2,
    draw_zorder, self.a)
  # 0.5, 0.5, # rotate around the center
  # 1, 1, # scaling factor
  # @color, # modify color
  # :add) # draw additively
end

#draw_zorderObject


346
# File 'lib/game_2d/entity.rb', line 346

def draw_zorder; ZOrder::Objects end

#drop_diagonal(x_vel, y_vel) ⇒ Object

Given a vector with a diagonal, drop the smaller component, returning a vector that is strictly either horizontal or vertical.


264
265
266
# File 'lib/game_2d/entity.rb', line 264

def drop_diagonal(x_vel, y_vel)
  (y_vel.abs > x_vel.abs) ? [0, y_vel] : [x_vel, 0]
end

#empty_above?Boolean

Returns:

  • (Boolean)

234
# File 'lib/game_2d/entity.rb', line 234

def empty_above?; opaque(next_to(0)).empty?; end

#empty_on_left?Boolean

Returns:

  • (Boolean)

232
# File 'lib/game_2d/entity.rb', line 232

def empty_on_left?; opaque(next_to(270)).empty?; end

#empty_on_right?Boolean

Returns:

  • (Boolean)

233
# File 'lib/game_2d/entity.rb', line 233

def empty_on_right?; opaque(next_to(90)).empty?; end

#empty_underneath?Boolean

Returns:

  • (Boolean)

231
# File 'lib/game_2d/entity.rb', line 231

def empty_underneath?; opaque(next_to(180)).empty?; end

#entities_obstructing(new_x, new_y) ⇒ Object

Wrapper around @space.entities_overlapping Allows us to remove any entities that are transparent to us


116
117
118
119
# File 'lib/game_2d/entity.rb', line 116

def entities_obstructing(new_x, new_y)
  fail "No @space set!" unless @space
  opaque(@space.entities_overlapping(new_x, new_y))
end

#going_past_entity(other_x, other_y) ⇒ Object

Given our current position and velocity (and only if our velocity is not on a diagonal), are we about to move past the entity at the specified coordinates? If so, returns:

1) The X/Y position of the empty space just past the entity. Assuming the other entity is adjacent to us, this spot touches corners with the other entity.

2) How far we’d go to reach that point.

3) How far past that spot we would go.

4) Which way we’d have to turn (delta angle) if moving around the other entity. Either +90 or -90.


288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
# File 'lib/game_2d/entity.rb', line 288

def going_past_entity(other_x, other_y)
  return if @x_vel == 0 && @y_vel == 0
  return if @x_vel != 0 && @y_vel != 0

  if @x_vel.zero?
    # Moving vertically.  Find target height
    y_pos = (@y_vel > 0) ? other_y + HEIGHT : other_y - HEIGHT
    distance = (@y - y_pos).abs
    overshoot = @y_vel.abs - distance
    turn = if @y_vel > 0
      # Going down: Turn left if it's on our right
      direction_to(other_x, other_y) == 90 ? -90 : 90
    else
      # Going up: Turn right if it's on our right
      direction_to(other_x, other_y) == 90 ? 90 : -90
    end
    return [[@x, y_pos], distance, overshoot, turn] if overshoot >= 0
  else
    # Moving horizontally.  Find target column
    x_pos = (@x_vel > 0) ? other_x + WIDTH : other_x - WIDTH
    distance = (@x - x_pos).abs
    overshoot = @x_vel.abs - distance
    turn = if @x_vel > 0
      # Going right: Turn right if it's below us
      direction_to(other_x, other_y) == 180 ? 90 : -90
    else
      # Going left: Turn left if it's below us
      direction_to(other_x, other_y) == 180 ? -90 : 90
    end
    return [[x_pos, @y], distance, overshoot, turn] if overshoot >= 0
  end
end

#harmed_by(other) ⇒ Object


213
# File 'lib/game_2d/entity.rb', line 213

def harmed_by(other); end

#i_hit(other) ⇒ Object


208
209
210
211
# File 'lib/game_2d/entity.rb', line 208

def i_hit(other)
  # TODO
  puts "#{self} hit #{other.inspect}"
end

#image_filenameObject


342
343
344
# File 'lib/game_2d/entity.rb', line 342

def image_filename
  raise "No image filename defined"
end

#left_cell_x(x = @x) ⇒ Object

TODO: Find a more elegant way to call class methods in Ruby…


74
# File 'lib/game_2d/entity.rb', line 74

def left_cell_x(x = @x); self.class.left_cell_x_at(x); end

#moveObject

Process one tick of motion. Only called when moving? is true


172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/game_2d/entity.rb', line 172

def move
  # Force evaluation of both update_x and update_y (no short-circuit)
  # If we're moving faster horizontally, do that first
  # Otherwise do the vertical move first
  moved = @space.process_moving_entity(self) do
    if @x_vel.abs > @y_vel.abs then move_x; move_y
    else move_y; move_x
    end
  end

  # Didn't move?  Might be time to go to sleep
  @moving = false if !moved && sleep_now?

  moved
end

#move_xObject

Process one tick of motion, horizontally only


122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/game_2d/entity.rb', line 122

def move_x
  return if doomed?
  return if @x_vel.zero?
  new_x = @x + @x_vel
  impacts = entities_obstructing(new_x, @y)
  if impacts.empty?
    @x = new_x
    return
  end
  @x = if @x_vel > 0 # moving right
    # X position of leftmost candidate(s)
    impact_at_x = impacts.collect(&:x).min
    impacts.delete_if {|e| e.x > impact_at_x }
    impact_at_x - WIDTH
  else # moving left
    # X position of rightmost candidate(s)
    impact_at_x = impacts.collect(&:x).max
    impacts.delete_if {|e| e.x < impact_at_x }
    impact_at_x + WIDTH
  end
  self.x_vel = 0
  i_hit(impacts)
end

#move_yObject

Process one tick of motion, vertically only


147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
# File 'lib/game_2d/entity.rb', line 147

def move_y
  return if doomed?
  return if @y_vel.zero?
  new_y = @y + @y_vel
  impacts = entities_obstructing(@x, new_y)
  if impacts.empty?
    @y = new_y
    return
  end
  @y = if @y_vel > 0 # moving down
    # Y position of highest candidate(s)
    impact_at_y = impacts.collect(&:y).min
    impacts.delete_if {|e| e.y > impact_at_y }
    impact_at_y - HEIGHT
  else # moving up
    # Y position of lowest candidate(s)
    impact_at_y = impacts.collect(&:y).max
    impacts.delete_if {|e| e.y < impact_at_y }
    impact_at_y + HEIGHT
  end
  self.y_vel = 0
  i_hit(impacts)
end

#moving?Boolean

True if we need to update this entity

Returns:

  • (Boolean)

43
# File 'lib/game_2d/entity.rb', line 43

def moving?; @moving; end

#next_to(angle, x = @x, y = @y) ⇒ Object

Return any entities adjacent to this one in the specified direction


216
217
218
219
220
221
222
223
224
225
226
227
228
229
# File 'lib/game_2d/entity.rb', line 216

def next_to(angle, x=@x, y=@y)
  points = case angle % 360
  when 0 then
    [[x, y - 1], [x + WIDTH - 1, y - 1]]
  when 90 then
    [[x + WIDTH, y], [x + WIDTH, y + HEIGHT - 1]]
  when 180 then
    [[x, y + HEIGHT], [x + WIDTH - 1, y + HEIGHT]]
  when 270 then
    [[x - 1, y], [x - 1, y + HEIGHT - 1]]
  else puts "Trig unimplemented"; []
  end
  @space.entities_at_points(points)
end

#occupied_cells(x = @x, y = @y) ⇒ Object

Returns an array of one, two, or four cell-coordinate tuples E.g. [[4, 5], [4, 6], [5, 5], [5, 6]]


92
93
94
95
96
# File 'lib/game_2d/entity.rb', line 92

def occupied_cells(x = @x, y = @y)
  x_array = (left_cell_x(x) .. right_cell_x(x)).to_a
  y_array = (top_cell_y(y) .. bottom_cell_y(y)).to_a
  x_array.product(y_array)
end

#opaque(others) ⇒ Object


109
110
111
# File 'lib/game_2d/entity.rb', line 109

def opaque(others)
  others.delete_if {|obj| transparent_to_me?(obj)}
end

#pixel_xObject

X positions near this entity’s Position in pixels of the upper-left corner


68
# File 'lib/game_2d/entity.rb', line 68

def pixel_x; @x / PIXEL_WIDTH; end

#pixel_yObject


69
# File 'lib/game_2d/entity.rb', line 69

def pixel_y; @y / PIXEL_WIDTH; end

#right_cell_x(x = @x) ⇒ Object


80
# File 'lib/game_2d/entity.rb', line 80

def right_cell_x(x = @x); self.class.right_cell_x_at(x); end

#should_fall?Boolean

Returns:

  • (Boolean)

54
55
56
# File 'lib/game_2d/entity.rb', line 54

def should_fall?
  raise "should_fall? undefined"
end

#sleep_now?Boolean

True if this entity can go to sleep now Only called if update() fails to produce any motion Default: Sleep if we’re not moving and not falling

Returns:

  • (Boolean)

50
51
52
# File 'lib/game_2d/entity.rb', line 50

def sleep_now?
  self.x_vel == 0 && self.y_vel == 0 && !should_fall?
end

#to_sObject


364
365
366
# File 'lib/game_2d/entity.rb', line 364

def to_s
  "#{self.class} (#{registry_id_safe}) at #{x}x#{y}"
end

#top_cell_y(y = @y) ⇒ Object


84
# File 'lib/game_2d/entity.rb', line 84

def top_cell_y(y = @y); self.class.top_cell_y_at(y); end

#transparent_to_me?(other) ⇒ Boolean

Override to make particular entities transparent to each other

Returns:

  • (Boolean)

105
106
107
# File 'lib/game_2d/entity.rb', line 105

def transparent_to_me?(other)
  other == self
end

#updateObject

Handle any behavior specific to this entity Default: Accelerate downward if the subclass says we should fall


190
191
192
193
# File 'lib/game_2d/entity.rb', line 190

def update
  accelerate(0, 1) if should_fall?
  move
end

#update_from_json(json) ⇒ Object


332
333
334
335
336
337
338
339
340
# File 'lib/game_2d/entity.rb', line 332

def update_from_json(json)
  new_x, new_y = json[:position]
  new_x_vel, new_y_vel = json[:velocity]
  new_angle = json[:angle]
  new_moving = json[:moving]

  warp(new_x, new_y, new_x_vel, new_y_vel, new_angle, new_moving)
  self
end

#vector_to_angle(x_vel = @x_vel, y_vel = @y_vel) ⇒ Object

Convert x/y to an angle


247
248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/game_2d/entity.rb', line 247

def vector_to_angle(x_vel=@x_vel, y_vel=@y_vel)
  if x_vel == 0 && y_vel == 0
    return puts "Zero velocity, no angle"
  end
  if x_vel != 0 && y_vel != 0
    return puts "Diagonal velocity (#{x_vel}x#{y_vel}), no angle"
  end

  if x_vel.zero?
    (y_vel > 0) ? 180 : 0
  else
    (x_vel > 0) ? 90 : 270
  end
end

#wake!Object

Notify this entity that it must take action


59
60
61
# File 'lib/game_2d/entity.rb', line 59

def wake!
  @moving = true
end

#warp(x, y, x_vel, y_vel, angle = self.a, moving = @moving) ⇒ Object

Update position/velocity/angle data, and tell the space about it


196
197
198
199
200
201
202
203
204
205
206
# File 'lib/game_2d/entity.rb', line 196

def warp(x, y, x_vel, y_vel, angle=self.a, moving=@moving)
  blk = proc do
    @x, @y, self.x_vel, self.y_vel, self.a, @moving =
      x, y, x_vel, y_vel, angle, moving
  end
  if @space
    @space.process_moving_entity(self, &blk)
  else
    blk.call
  end
end