source: nagslang/game_object.py@ 186:d63c19003aec

Last change on this file since 186:d63c19003aec was 186:d63c19003aec, checked in by Jeremy Thurgood <firxen@…>, 7 years ago

Some refactoring and fixing, start of better collision handling.

File size: 12.6 KB
Line 
1import math
2
3import pygame
4import pymunk
5import pymunk.pygame_util
6
7from nagslang.constants import (
8 SWITCH_PUSHERS, COLLISION_TYPE_SWITCH, COLLISION_TYPE_BOX, ZORDER_LOW,
9 ZORDER_FLOOR, COLLISION_TYPE_DOOR, COLLISION_TYPE_PLAYER)
10from nagslang.options import options
11from nagslang.resources import resources
12from nagslang.events import DoorEvent
13
14
15class PuzzleGlue(object):
16 """Glue that holds bits of a puzzle together.
17 """
18 def __init__(self):
19 self._components = {}
20
21 def add_component(self, name, puzzler):
22 if not isinstance(puzzler, Puzzler):
23 puzzler = puzzler.puzzler
24 self._components[name] = puzzler
25 puzzler.set_glue(self)
26
27 def get_state_of(self, name):
28 return self._components[name].get_state()
29
30
31class Puzzler(object):
32 """Behaviour specific to a puzzle component.
33 """
34 def set_glue(self, glue):
35 self.glue = glue
36
37 def set_game_object(self, game_object):
38 self.game_object = game_object
39
40 def get_state(self):
41 raise NotImplementedError()
42
43
44class YesPuzzler(Puzzler):
45 """Yes sir, I'm always on.
46 """
47 def get_state(self):
48 return True
49
50
51class NoPuzzler(Puzzler):
52 """No sir, I'm always off.
53 """
54 def get_state(self):
55 return False
56
57
58class CollidePuzzler(Puzzler):
59 def __init__(self, *collision_types):
60 if not collision_types:
61 collision_types = (COLLISION_TYPE_PLAYER,)
62 self._collision_types = collision_types
63
64 def get_state(self):
65 space = self.game_object.get_space()
66 for shape in space.shape_query(self.game_object.get_shape()):
67 if shape.collision_type in self._collision_types:
68 return True
69 return False
70
71
72class StateProxyPuzzler(Puzzler):
73 def __init__(self, state_source):
74 self._state_source = state_source
75
76 def get_state(self):
77 return self.glue.get_state_of(self._state_source)
78
79
80class StateLogicalAndPuzzler(Puzzler):
81 def __init__(self, *state_sources):
82 self._state_sources = state_sources
83
84 def get_state(self):
85 for state_source in self._state_sources:
86 if not self.glue.get_state_of(state_source):
87 return False
88 return True
89
90
91class Physicser(object):
92 def __init__(self, space):
93 self._space = space
94
95 def get_space(self):
96 return self._space
97
98 def set_game_object(self, game_object):
99 self.game_object = game_object
100
101 def get_shape(self):
102 raise NotImplementedError()
103
104 def add_to_space(self):
105 raise NotImplementedError()
106
107 def remove_from_space(self):
108 raise NotImplementedError()
109
110 def get_render_position(self, surface):
111 raise NotImplementedError()
112
113 def get_angle(self):
114 raise NotImplementedError()
115
116 def apply_impulse(self, j, r=(0, 0)):
117 raise NotImplementedError()
118
119
120class SingleShapePhysicser(Physicser):
121 def __init__(self, space, shape):
122 super(SingleShapePhysicser, self).__init__(space)
123 self._shape = shape
124 shape.physicser = self
125
126 def get_shape(self):
127 return self._shape
128
129 def add_to_space(self):
130 self.get_space().add(self._shape)
131 if not self._shape.body.is_static:
132 self.get_space().add(self._shape.body)
133
134 def remove_from_space(self):
135 self.get_space().remove(self._shape)
136 if not self._shape.body.is_static:
137 self.get_space().remove(self._shape.body)
138
139 def get_render_position(self, surface):
140 pos = self._shape.body.position
141 return pymunk.pygame_util.to_pygame(pos, surface)
142
143 def get_angle(self):
144 return self._shape.body.angle
145
146 def apply_impulse(self, j, r=(0, 0)):
147 return self._shape.body.apply_impulse(j, r)
148
149
150class Renderer(object):
151 def set_game_object(self, game_object):
152 self.game_object = game_object
153
154 def _render_shape(self, surface):
155 shape = self.game_object.get_shape()
156 # Less general that pymunk.pygame_util.draw, but also a lot less noisy.
157 color = getattr(shape, 'color', pygame.color.THECOLORS['lightblue'])
158 # We only explicitly draw Circle and Poly shapes. Everything else we
159 # forward to pymunk.
160 if isinstance(shape, pymunk.Circle):
161 centre = pymunk.pygame_util.to_pygame(shape.body.position, surface)
162 radius = int(shape.radius)
163 pygame.draw.circle(surface, color, centre, radius, 2)
164 elif isinstance(shape, pymunk.Poly):
165 # polygon bounding box
166 points = [pymunk.pygame_util.to_pygame(p, surface)
167 for p in shape.get_vertices()]
168 pygame.draw.lines(surface, color, True, points, 2)
169 else:
170 pymunk.pygame_util.draw(surface, shape)
171
172 def render(self, surface):
173 if options.debug:
174 self._render_shape(surface)
175
176 def animate(self):
177 # Used by time animatations to advance the clock
178 pass
179
180
181def image_pos(image, pos):
182 return (pos[0] - image.get_width() / 2,
183 pos[1] - image.get_height() / 2)
184
185
186class ImageRenderer(Renderer):
187 def __init__(self, image):
188 self._image = image
189
190 def get_image(self):
191 return self._image
192
193 def rotate_image(self, image):
194 angle = self.game_object.get_render_angle() * 180 / math.pi
195 return pygame.transform.rotate(image, angle)
196
197 def render_image(self, surface, image):
198 image = self.rotate_image(image)
199 pos = self.game_object.get_render_position(surface)
200 surface.blit(image, image_pos(image, pos))
201
202 def render(self, surface):
203 self.render_image(surface, self.get_image())
204 super(ImageRenderer, self).render(surface)
205
206
207class ImageStateRenderer(ImageRenderer):
208 def __init__(self, state_images):
209 self._state_images = state_images
210
211 def get_image(self):
212 return self._state_images[self.game_object.puzzler.get_state()]
213
214
215class FacingImageRenderer(ImageRenderer):
216 def __init__(self, left_image, right_image):
217 self._images = {
218 'left': left_image,
219 'right': right_image,
220 }
221 self._face = 'left'
222
223 def _update_facing(self, angle):
224 if abs(angle) < math.pi / 2:
225 self._face = 'right'
226 elif abs(angle) > math.pi / 2:
227 self._face = 'left'
228
229 def rotate_image(self, image):
230 # Facing images don't get rotated.
231 return image
232
233 def get_facing_image(self):
234 return self._images[self._face]
235
236 def get_image(self):
237 angle = self.game_object.get_render_angle()
238 self._update_facing(angle)
239 return self.get_facing_image()
240
241
242class AnimatedFacingImageRenderer(FacingImageRenderer):
243 def __init__(self, left_images, right_images):
244 self._images = {
245 'left': left_images,
246 'right': right_images,
247 }
248 self._frame = 0
249 self._moving = False
250 self._face = 'left'
251
252 def get_facing_image(self):
253 if self._frame >= len(self._images[self._face]):
254 self._frame = 0
255 return self._images[self._face][self._frame]
256
257 def animate(self):
258 if self._moving:
259 self._frame += 1
260 else:
261 self._frame = 0
262
263 def start(self):
264 self._moving = True
265
266 def stop(self):
267 self._moving = False
268
269
270class TimedAnimatedRenderer(ImageRenderer):
271
272 def __init__(self, images):
273 self._images = images
274 self._frame = 0
275 self._image = None
276
277 def get_image(self):
278 if self._frame > len(self._imaages):
279 self._frame = 0
280 return self._images[self._frame]
281
282 def animate(self):
283 self._frame += 1
284
285
286class ShapeRenderer(Renderer):
287 def render(self, surface):
288 self._render_shape(surface)
289 super(ShapeRenderer, self).render(surface)
290
291
292class ShapeStateRenderer(ShapeRenderer):
293 """Renders the shape in a different colour depending on the state.
294
295 Requires the game object it's attached to to have a puzzler.
296 """
297 def render(self, surface):
298 if self.game_object.puzzler.get_state():
299 color = pygame.color.THECOLORS['green']
300 else:
301 color = pygame.color.THECOLORS['red']
302
303 self.game_object.get_shape().color = color
304 super(ShapeStateRenderer, self).render(surface)
305
306
307def damping_velocity_func(body, gravity, damping, dt):
308 """Apply custom damping to this body's velocity.
309 """
310 damping = getattr(body, 'damping', damping)
311 return pymunk.Body.update_velocity(body, gravity, damping, dt)
312
313
314def make_body(mass, moment, position, damping=None):
315 body = pymunk.Body(mass, moment)
316 body.position = tuple(position)
317 if damping is not None:
318 body.damping = damping
319 body.velocity_func = damping_velocity_func
320 return body
321
322
323class GameObject(object):
324 """A representation of a thing in the game world.
325
326 This has a rendery thing, physicsy things and maybe some other things.
327 """
328
329 zorder = ZORDER_LOW
330
331 def __init__(self, physicser, renderer, puzzler=None):
332 self.physicser = physicser
333 physicser.set_game_object(self)
334 self.physicser.add_to_space()
335 self.renderer = renderer
336 renderer.set_game_object(self)
337 self.puzzler = puzzler
338 if puzzler is not None:
339 puzzler.set_game_object(self)
340
341 def get_space(self):
342 return self.physicser.get_space()
343
344 def get_shape(self):
345 return self.physicser.get_shape()
346
347 def get_render_position(self, surface):
348 return self.physicser.get_render_position(surface)
349
350 def get_render_angle(self):
351 return self.physicser.get_angle()
352
353 def render(self, surface):
354 return self.renderer.render(surface)
355
356 def animate(self):
357 self.renderer.animate()
358
359 def collide_with_protagonist(self):
360 """Called as a `pre_solve` collision callback with the protagonist.
361
362 You can return `False` to ignore the collision, anything else
363 (including `None`) to process the collision as normal.
364 """
365 pass
366
367
368class FloorSwitch(GameObject):
369 zorder = ZORDER_FLOOR
370
371 def __init__(self, space, position):
372 body = make_body(None, None, position)
373 self.shape = pymunk.Circle(body, 30)
374 self.shape.collision_type = COLLISION_TYPE_SWITCH
375 self.shape.sensor = True
376 super(FloorSwitch, self).__init__(
377 SingleShapePhysicser(space, self.shape),
378 ImageStateRenderer({
379 True: resources.get_image('objects', 'sensor_on.png'),
380 False: resources.get_image('objects', 'sensor_off.png'),
381 }),
382 CollidePuzzler(*SWITCH_PUSHERS),
383 )
384
385
386class FloorLight(GameObject):
387 zorder = ZORDER_FLOOR
388
389 def __init__(self, space, position, state_source):
390 body = make_body(None, None, position)
391 self.shape = pymunk.Circle(body, 10)
392 self.shape.collision_type = COLLISION_TYPE_SWITCH
393 self.shape.sensor = True
394 super(FloorLight, self).__init__(
395 SingleShapePhysicser(space, self.shape),
396 ImageStateRenderer({
397 True: resources.get_image('objects', 'light_on.png'),
398 False: resources.get_image('objects', 'light_off.png'),
399 }),
400 StateProxyPuzzler(state_source),
401 )
402
403
404class Box(GameObject):
405 def __init__(self, space, position):
406 body = make_body(10, 10000, position, damping=0.5)
407 self.shape = pymunk.Poly(
408 body, [(-20, -20), (20, -20), (20, 20), (-20, 20)])
409 self.shape.collision_type = COLLISION_TYPE_BOX
410 super(Box, self).__init__(
411 SingleShapePhysicser(space, self.shape),
412 ImageRenderer(resources.get_image('objects', 'crate.png')),
413 )
414
415
416class Door(GameObject):
417 zorder = ZORDER_FLOOR
418
419 def __init__(self, space, position, destination, dest_pos, key_state=None):
420 body = make_body(pymunk.inf, pymunk.inf, position, damping=0.5)
421 self.shape = pymunk.Poly(
422 body, [(-32, -32), (32, -32), (32, 32), (-32, 32)])
423 self.shape.collision_type = COLLISION_TYPE_DOOR
424 self.shape.sensor = True
425 self.destination = destination
426 self.dest_pos = tuple(dest_pos)
427 if key_state is None:
428 puzzler = YesPuzzler()
429 else:
430 puzzler = StateProxyPuzzler(key_state)
431 super(Door, self).__init__(
432 SingleShapePhysicser(space, self.shape),
433 ImageRenderer(resources.get_image('objects', 'door.png')),
434 puzzler,
435 )
436
437 def animate(self):
438 space = self.get_space()
439 for shape in space.shape_query(self.get_shape()):
440 if shape.collision_type == COLLISION_TYPE_PLAYER:
441 # Force to new position
442 DoorEvent.post(self.destination, self.dest_pos)
Note: See TracBrowser for help on using the repository browser.