nettoyage / implémentation de la méthode update.
[minwii.git] / src / pgu / vid.py
1 """Sprite and tile engine.
2
3 <p>[[tilevid]], [[isovid]], [[hexvid]] are all subclasses of
4 this interface.</p>
5
6 <p>Includes support for:</p>
7
8 <ul>
9 <li> Foreground Tiles
10 <li> Background Tiles
11 <li> Sprites
12 <li> Sprite-Sprite Collision handling
13 <li> Sprite-Tile Collision handling
14 <li> Scrolling
15 <li> Loading from PGU tile and sprite formats (optional)
16 <li> Set rate FPS (optional)
17 </ul>
18
19 <p>This code was previously known as the King James Version (named after the
20 Bible of the same name for historical reasons.)</p>
21 """
22
23 import pygame
24 from pygame.rect import Rect
25 from pygame.locals import *
26 import math
27
28 class Sprite:
29 """The object used for Sprites.
30
31 <pre>Sprite(ishape,pos)</pre>
32
33 <dl>
34 <dt>ishape <dd>an image, or an image, rectstyle. The rectstyle will
35 describe the shape of the image, used for collision
36 detection.
37 <dt>pos <dd>initial (x,y) position of the Sprite.
38 </dl>
39
40 <strong>Attributes</strong>
41 <dl>
42 <dt>rect <dd>the current position of the Sprite
43 <dt>_rect <dd>the previous position of the Sprite
44 <dt>groups <dd>the groups the Sprite is in
45 <dt>agroups <dd>the groups the Sprite can hit in a collision
46 <dt>hit <dd>the handler for hits -- hit(g,s,a)
47 <dt>loop <dd>the loop handler, called once a frame
48 </dl>
49 """
50 def __init__(self,ishape,pos):
51 if not isinstance(ishape, tuple):
52 ishape = ishape,None
53 image,shape = ishape
54 if shape == None:
55 shape = pygame.Rect(0,0,image.get_width(),image.get_height())
56 if isinstance(shape, tuple): shape = pygame.Rect(shape)
57 self.image = image
58 self._image = self.image
59 self.shape = shape
60 self.rect = pygame.Rect(pos[0],pos[1],shape.w,shape.h)
61 self._rect = pygame.Rect(self.rect)
62 self.irect = pygame.Rect(pos[0]-self.shape.x,pos[1]-self.shape.y,
63 image.get_width(),image.get_height())
64 self._irect = pygame.Rect(self.irect)
65 self.groups = 0
66 self.agroups = 0
67 self.updated = 1
68
69 def setimage(self,ishape):
70 """Set the image of the Sprite.
71
72 <pre>Sprite.setimage(ishape)</pre>
73
74 <dl>
75 <dt>ishape <dd>an image, or an image, rectstyle. The rectstyle will
76 describe the shape of the image, used for collision detection.
77 </dl>
78 """
79 if not isinstance(ishape, tuple):
80 ishape = ishape,None
81 image,shape = ishape
82 if shape == None:
83 shape = pygame.Rect(0,0,image.get_width(),image.get_height())
84 if isinstance(shape, tuple):
85 shape = pygame.Rect(shape)
86 self.image = image
87 self.shape = shape
88 self.rect.w,self.rect.h = shape.w,shape.h
89 self.irect.w,self.irect.h = image.get_width(),image.get_height()
90 self.updated = 1
91
92
93 class Tile:
94 """Tile Object used by TileCollide.
95
96 <pre>Tile(image=None)</pre>
97 <dl>
98 <dt>image <dd>an image for the Tile.
99 </dl>
100
101 <strong>Attributes</strong>
102 <dl>
103 <dt>agroups <dd>the groups the Tile can hit in a collision
104 <dt>hit <dd>the handler for hits -- hit(g,t,a)
105 </dl>
106 """
107 def __init__(self,image=None):
108 self.image = image
109 self.agroups = 0
110
111 def __setattr__(self,k,v):
112 if k == 'image' and v != None:
113 self.image_h = v.get_height()
114 self.image_w = v.get_width()
115 self.__dict__[k] = v
116
117 class _Sprites(list):
118 def __init__(self):
119 list.__init__(self)
120 self.removed = []
121
122 def append(self,v):
123 list.append(self,v)
124 v.updated = 1
125
126 def remove(self,v):
127 list.remove(self,v)
128 v.updated = 1
129 self.removed.append(v)
130
131 class Vid:
132 """An engine for rendering Sprites and Tiles.
133
134 <pre>Vid()</pre>
135
136 <strong>Attributes</strong>
137 <dl>
138 <dt>sprites <dd>a list of the Sprites to be displayed. You may append and
139 remove Sprites from it.
140 <dt>images <dd>a dict for images to be put in.
141 <dt>size <dd>the width, height in Tiles of the layers. Do not modify.
142 <dt>view <dd>a pygame.Rect of the viewed area. You may change .x, .y,
143 etc to move the viewed area around.
144 <dt>bounds <dd>a pygame.Rect (set to None by default) that sets the bounds
145 of the viewable area. Useful for setting certain borders
146 as not viewable.
147 <dt>tlayer <dd>the foreground tiles layer
148 <dt>clayer <dd>the code layer (optional)
149 <dt>blayer <dd>the background tiles layer (optional)
150 <dt>groups <dd>a hash of group names to group values (32 groups max, as a tile/sprites
151 membership in a group is determined by the bits in an integer)
152 </dl>
153 """
154
155 def __init__(self):
156 self.tiles = [None for x in xrange(0,256)]
157 self.sprites = _Sprites()
158 self.images = {} #just a store for images.
159 self.layers = None
160 self.size = None
161 self.view = pygame.Rect(0,0,0,0)
162 self._view = pygame.Rect(self.view)
163 self.bounds = None
164 self.updates = []
165 self.groups = {}
166
167
168 def resize(self,size,bg=0):
169 """Resize the layers.
170
171 <pre>Vid.resize(size,bg=0)</pre>
172
173 <dl>
174 <dt>size <dd>w,h in Tiles of the layers
175 <dt>bg <dd>set to 1 if you wish to use both a foreground layer and a
176 background layer
177 </dl>
178 """
179 self.size = size
180 w,h = size
181 self.layers = [[[0 for x in xrange(0,w)] for y in xrange(0,h)]
182 for z in xrange(0,4)]
183 self.tlayer = self.layers[0]
184 self.blayer = self.layers[1]
185 if not bg: self.blayer = None
186 self.clayer = self.layers[2]
187 self.alayer = self.layers[3]
188
189 self.view.x, self.view.y = 0,0
190 self._view.x, self.view.y = 0,0
191 self.bounds = None
192
193 self.updates = []
194
195 def set(self,pos,v):
196 """Set a tile in the foreground to a value.
197
198 <p>Use this method to set tiles in the foreground, as it will make
199 sure the screen is updated with the change. Directly changing
200 the tlayer will not guarantee updates unless you are using .paint()
201 </p>
202
203 <pre>Vid.set(pos,v)</pre>
204
205 <dl>
206 <dt>pos <dd>(x,y) of tile
207 <dt>v <dd>value
208 </dl>
209 """
210 if self.tlayer[pos[1]][pos[0]] == v: return
211 self.tlayer[pos[1]][pos[0]] = v
212 self.alayer[pos[1]][pos[0]] = 1
213 self.updates.append(pos)
214
215 def get(self,pos):
216 """Get the tlayer at pos.
217
218 <pre>Vid.get(pos): return value</pre>
219
220 <dl>
221 <dt>pos <dd>(x,y) of tile
222 </dl>
223 """
224 return self.tlayer[pos[1]][pos[0]]
225
226 def paint(self,s):
227 """Paint the screen.
228
229 <pre>Vid.paint(screen): return [updates]</pre>
230
231 <dl>
232 <dt>screen <dd>a pygame.Surface to paint to
233 </dl>
234
235 <p>returns the updated portion of the screen (all of it)</p>
236 """
237 return []
238
239 def update(self,s):
240 """Update the screen.
241
242 <pre>Vid.update(screen): return [updates]</pre>
243
244 <dl>
245 <dt>screen <dd>a pygame.Rect to update
246 </dl>
247
248 <p>returns a list of updated rectangles.</p>
249 """
250 self.updates = []
251 return []
252
253 def tga_load_level(self,fname,bg=0):
254 """Load a TGA level.
255
256 <pre>Vid.tga_load_level(fname,bg=0)</pre>
257
258 <dl>
259 <dt>g <dd>a Tilevid instance
260 <dt>fname <dd>tga image to load
261 <dt>bg <dd>set to 1 if you wish to load the background layer
262 </dl>
263 """
264 if type(fname) == str: img = pygame.image.load(fname)
265 else: img = fname
266 w,h = img.get_width(),img.get_height()
267 self.resize((w,h),bg)
268 for y in range(0,h):
269 for x in range(0,w):
270 t,b,c,_a = img.get_at((x,y))
271 self.tlayer[y][x] = t
272 if bg: self.blayer[y][x] = b
273 self.clayer[y][x] = c
274
275 def tga_save_level(self,fname):
276 """Save a TGA level.
277
278 <pre>Vid.tga_save_level(fname)</pre>
279
280 <dl>
281 <dt>fname <dd>tga image to save to
282 </dl>
283 """
284 w,h = self.size
285 img = pygame.Surface((w,h),SWSURFACE,32)
286 img.fill((0,0,0,0))
287 for y in range(0,h):
288 for x in range(0,w):
289 t = self.tlayer[y][x]
290 b = 0
291 if self.blayer:
292 b = self.blayer[y][x]
293 c = self.clayer[y][x]
294 _a = 0
295 img.set_at((x,y),(t,b,c,_a))
296 pygame.image.save(img,fname)
297
298
299
300 def tga_load_tiles(self,fname,size,tdata={}):
301 """Load a TGA tileset.
302
303 <pre>Vid.tga_load_tiles(fname,size,tdata={})</pre>
304
305 <dl>
306 <dt>g <dd>a Tilevid instance
307 <dt>fname <dd>tga image to load
308 <dt>size <dd>(w,h) size of tiles in pixels
309 <dt>tdata <dd>tile data, a dict of tile:(agroups, hit handler, config)
310 </dl>
311 """
312 TW,TH = size
313 if type(fname) == str: img = pygame.image.load(fname).convert_alpha()
314 else: img = fname
315 w,h = img.get_width(),img.get_height()
316
317 n = 0
318 for y in range(0,h,TH):
319 for x in range(0,w,TW):
320 i = img.subsurface((x,y,TW,TH))
321 tile = Tile(i)
322 self.tiles[n] = tile
323 if n in tdata:
324 agroups,hit,config = tdata[n]
325 tile.agroups = self.string2groups(agroups)
326 tile.hit = hit
327 tile.config = config
328 n += 1
329
330
331 def load_images(self,idata):
332 """Load images.
333
334 <pre>Vid.load_images(idata)</pre>
335
336 <dl>
337 <dt>idata <dd>a list of (name, fname, shape)
338 </dl>
339 """
340 for name,fname,shape in idata:
341 self.images[name] = pygame.image.load(fname).convert_alpha(),shape
342
343 def run_codes(self,cdata,rect):
344 """Run codes.
345
346 <pre>Vid.run_codes(cdata,rect)</pre>
347
348 <dl>
349 <dt>cdata <dd>a dict of code:(handler function, value)
350 <dt>rect <dd>a tile rect of the parts of the layer that should have
351 their codes run
352 </dl>
353 """
354 tw,th = self.tiles[0].image.get_width(),self.tiles[0].image.get_height()
355
356 x1,y1,w,h = rect
357 clayer = self.clayer
358 t = Tile()
359 for y in range(y1,y1+h):
360 for x in range(x1,x1+w):
361 n = clayer[y][x]
362 if n in cdata:
363 fnc,value = cdata[n]
364 t.tx,t.ty = x,y
365 t.rect = pygame.Rect(x*tw,y*th,tw,th)
366 fnc(self,t,value)
367
368
369 def string2groups(self,str):
370 """Convert a string to groups.
371
372 <pre>Vid.string2groups(str): return groups</pre>
373 """
374 if str == None: return 0
375 return self.list2groups(str.split(","))
376
377 def list2groups(self,igroups):
378 """Convert a list to groups.
379 <pre>Vid.list2groups(igroups): return groups</pre>
380 """
381 for s in igroups:
382 if not s in self.groups:
383 self.groups[s] = 2**len(self.groups)
384 v = 0
385 for s,n in self.groups.items():
386 if s in igroups: v|=n
387 return v
388
389 def groups2list(self,groups):
390 """Convert a groups to a list.
391 <pre>Vid.groups2list(groups): return list</pre>
392 """
393 v = []
394 for s,n in self.groups.items():
395 if (n&groups)!=0: v.append(s)
396 return v
397
398 def hit(self,x,y,t,s):
399 tiles = self.tiles
400 tw,th = tiles[0].image.get_width(),tiles[0].image.get_height()
401 t.tx = x
402 t.ty = y
403 t.rect = Rect(x*tw,y*th,tw,th)
404 t._rect = t.rect
405 if hasattr(t,'hit'):
406 t.hit(self,t,s)
407
408 def loop(self):
409 """Update and hit testing loop. Run this once per frame.
410 <pre>Vid.loop()</pre>
411 """
412 self.loop_sprites() #sprites may move
413 self.loop_tilehits() #sprites move
414 self.loop_spritehits() #no sprites should move
415 for s in self.sprites:
416 s._rect = pygame.Rect(s.rect)
417
418 def loop_sprites(self):
419 as_ = self.sprites[:]
420 for s in as_:
421 if hasattr(s,'loop'):
422 s.loop(self,s)
423
424 def loop_tilehits(self):
425 tiles = self.tiles
426 tw,th = tiles[0].image.get_width(),tiles[0].image.get_height()
427
428 layer = self.layers[0]
429
430 as_ = self.sprites[:]
431 for s in as_:
432 self._tilehits(s)
433
434 def _tilehits(self,s):
435 tiles = self.tiles
436 tw,th = tiles[0].image.get_width(),tiles[0].image.get_height()
437 layer = self.layers[0]
438
439 for _z in (0,):
440 if s.groups != 0:
441
442 _rect = s._rect
443 rect = s.rect
444
445 _rectx = _rect.x
446 _recty = _rect.y
447 _rectw = _rect.w
448 _recth = _rect.h
449
450 rectx = rect.x
451 recty = rect.y
452 rectw = rect.w
453 recth = rect.h
454
455 rect.y = _rect.y
456 rect.h = _rect.h
457
458 hits = []
459 ct,cb,cl,cr = rect.top,rect.bottom,rect.left,rect.right
460 #nasty ol loops
461 y = ct/th*th
462 while y < cb:
463 x = cl/tw*tw
464 yy = y/th
465 while x < cr:
466 xx = x/tw
467 t = tiles[layer[yy][xx]]
468 if (s.groups & t.agroups)!=0:
469 #self.hit(xx,yy,t,s)
470 d = math.hypot(rect.centerx-(xx*tw+tw/2),
471 rect.centery-(yy*th+th/2))
472 hits.append((d,t,xx,yy))
473
474 x += tw
475 y += th
476
477 hits.sort()
478 #if len(hits) > 0: print self.frame,hits
479 for d,t,xx,yy in hits:
480 self.hit(xx,yy,t,s)
481
482 #switching directions...
483 _rect.x = rect.x
484 _rect.w = rect.w
485 rect.y = recty
486 rect.h = recth
487
488 hits = []
489 ct,cb,cl,cr = rect.top,rect.bottom,rect.left,rect.right
490 #nasty ol loops
491 y = ct/th*th
492 while y < cb:
493 x = cl/tw*tw
494 yy = y/th
495 while x < cr:
496 xx = x/tw
497 t = tiles[layer[yy][xx]]
498 if (s.groups & t.agroups)!=0:
499 d = math.hypot(rect.centerx-(xx*tw+tw/2),
500 rect.centery-(yy*th+th/2))
501 hits.append((d,t,xx,yy))
502 #self.hit(xx,yy,t,s)
503 x += tw
504 y += th
505
506 hits.sort()
507 #if len(hits) > 0: print self.frame,hits
508 for d,t,xx,yy in hits:
509 self.hit(xx,yy,t,s)
510
511 #done with loops
512 _rect.x = _rectx
513 _rect.y = _recty
514
515
516 def loop_spritehits(self):
517 as_ = self.sprites[:]
518
519 groups = {}
520 for n in range(0,31):
521 groups[1<<n] = []
522 for s in as_:
523 g = s.groups
524 n = 1
525 while g:
526 if (g&1)!=0: groups[n].append(s)
527 g >>= 1
528 n <<= 1
529
530 for s in as_:
531 if s.agroups!=0:
532 rect1,rect2 = s.rect,Rect(s.rect)
533 #if rect1.centerx < 320: rect2.x += 640
534 #else: rect2.x -= 640
535 g = s.agroups
536 n = 1
537 while g:
538 if (g&1)!=0:
539 for b in groups[n]:
540 if (s != b and (s.agroups & b.groups)!=0
541 and s.rect.colliderect(b.rect)):
542 s.hit(self,s,b)
543
544 g >>= 1
545 n <<= 1
546
547
548 def screen_to_tile(self,pos):
549 """Convert a screen position to a tile position.
550 <pre>Vid.screen_to_tile(pos): return pos</pre>
551 """
552 return pos
553
554 def tile_to_screen(self,pos):
555 """Convert a tile position to a screen position.
556 <pre>Vid.tile_to_screen(pos): return pos</pre>
557 """
558 return pos
559
560 # vim: set filetype=python sts=4 sw=4 noet si :