Last active
November 16, 2021 11:04
-
-
Save Mega-JC/6b67083d0f94d4bfc4a83f063d8724f5 to your computer and use it in GitHub Desktop.
Basic 2D Camera System for pygame using LayeredUpdates and a Camera class.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Two user-friendly classes for integrating a 2D camera system for pygame. | |
# | |
# The first class is Camera2, which is essentially a 2D camera with rect-like behaviour. To setup something like player tracking, one can simply do: | |
### Cam2 = Camera2(area=(0, 0, WIDTH, HEIGHT), view_distance=1000) | |
### Cam2.center = player.rect.center | |
# for even more precise control, one can do: | |
### Cam2.anchor = (Cam2.w*0.3, Cam2.h*0.7) # some coordinate on the screen | |
### Cam2.goto(player.rect.center) # moves the camera's world coordinate to the player's center | |
# The second class is CameraView, which is a subclass of pygame.sprite.LayeredUpdates . | |
# | |
# This is where all the scrolling is implemented, without ever messing with the actual sprite coordinates. | |
# For something to be visible in the camera's view, one can simply add those sprites into the camera's CameraView group: | |
### Cam2.view.add(Background1, Background2, player, monster1, monster2, ...) | |
# Note however, that each sprite needs to have a .distance attribute that is set to a positive real number. This is required for parallax effects to work. | |
# Each Camera2 object has a .view_distance property, which also is set to a positive real number. | |
# To finally draw sprites with the right displacement, one writes: | |
# Cam2.draw(surface, parallax=1, return_dirty_rects=1) | |
# This subsequently calls the draw() method of the CameraView object: | |
# with parallax set to 1: | |
#All sprites are displaced, with the strength based on their distance relative to the camera's view distance: clamp( (spr.distance/Cam2.view_distance) , 0.0, 1.0) | |
# with parallax set to 0: | |
#All sprites are displaced, at maximum strength. | |
# if a sprite's rect (after displacement) is still visible in the camera's area on the screen (using Rect.colliderect()), its image will be blitted unto the camera's view unto the destination surface, otherwise it won't. | |
# Multi Camera system: | |
#This system also supports multiple cameras on the screen, as long as their areas do not overlap: | |
### Cam = Camera2(area=(0, 0, WIDTH//2, HEIGHT), view_distance=1000) | |
### Cam2 = Camera2(area=(WIDTH//2, 0, WIDTH//2, HEIGHT), view=Cam.view, view_distance=1000) # CameraView objects can be shared | |
### | |
### Cam.view.add(Background1, Background2, player, monster1, monster2, ...) | |
### Cam.draw(surface, parallax=1, return_dirty_rects=1) | |
### Cam2.draw(surface, parallax=1, return_dirty_rects=1) | |
from pygame.sprite import LayeredUpdates, Sprite | |
from pygame import Surface, Rect | |
import pygame | |
RECT_INT_MIN = -2**15 | |
RECT_INT_MAX = 2**15-1 | |
max2, min2 = (lambda a, b: a if a > b else b), (lambda a, b: b if a > b else a) # faster than standard min() and max() | |
def clamp(a, mn=0.0, mx=1.0): | |
"""limits the input number a to the range ( mn < a < mx ). | |
""" | |
return mn if a < mn else mx if a > mx else a | |
class CameraView(LayeredUpdates): | |
"""subclass of LayeredUpdates specialized for use in Camera2 objects.""" | |
def add_internal(self, sprite, layer=None): | |
"""Do not use this method directly. | |
It is used by the group to add a sprite internally. | |
""" | |
self.spritedict[sprite] = self._init_rect | |
if layer is None: | |
try: | |
layer = sprite._layer | |
except AttributeError: | |
layer = sprite._layer = self._default_layer | |
elif hasattr(sprite, '_layer'): | |
sprite._layer = layer | |
try: | |
sprite.distance += 0 | |
except AttributeError as a: | |
a.args = ("each sprite passed to a CameraView group must have a (.distance) attribute with a positive real number as a value.",) | |
raise | |
except TypeError as t: | |
t.args = ("each sprite passed to a CameraView group must have a (.distance) attribute with a positive real number as a value.",) | |
raise | |
sprites = self._spritelist # speedup | |
sprites_layers = self._spritelayers | |
sprites_layers[sprite] = layer | |
# add the sprite at the right position | |
# bisect algorithmus | |
leng = len(sprites) | |
low = mid = 0 | |
high = leng - 1 | |
while low <= high: | |
mid = low + (high - low) // 2 | |
if sprites_layers[sprites[mid]] <= layer: | |
low = mid + 1 | |
else: | |
high = mid - 1 | |
# linear search to find final position | |
while mid < leng and sprites_layers[sprites[mid]] <= layer: | |
mid += 1 | |
sprites.insert(mid, sprite) | |
def add(self, *sprites, **kwargs): | |
"""add a sprite or sequence of sprites to a group | |
LayeredUpdates.add(*sprites, **kwargs): return None | |
Each sprite must have a (.distance) attribute set to a real number above or equal to zero. | |
If the sprite you add has an attribute _layer, then that layer will be | |
used. If **kwarg contains 'layer', then the passed sprites will be | |
added to that layer (overriding the sprite._layer attribute). If | |
neither the sprite nor **kwarg has a 'layer', then the default layer is | |
used to add the sprites. | |
""" | |
if not sprites: | |
return | |
if 'layer' in kwargs: | |
layer = kwargs['layer'] | |
else: | |
layer = None | |
for sprite in sprites: | |
# It's possible that some sprite is also an iterator. | |
# If this is the case, we should add the sprite itself, | |
# and not the iterator object. | |
if isinstance(sprite, Sprite): | |
if not self.has_internal(sprite): | |
self.add_internal(sprite, layer) | |
sprite.add_internal(self) | |
else: | |
try: | |
# See if sprite is an iterator, like a list or sprite | |
# group. | |
self.add(*sprite, **kwargs) | |
except (TypeError, AttributeError): | |
# Not iterable. This is probably a sprite that is not an | |
# instance of the Sprite class or is not an instance of a | |
# subclass of the Sprite class. Alternately, it could be an | |
# old-style sprite group. | |
if hasattr(sprite, '_spritegroup'): | |
for spr in sprite.sprites(): | |
if not self.has_internal(spr): | |
self.add_internal(spr, layer) | |
spr.add_internal(self) | |
elif not self.has_internal(sprite): | |
self.add_internal(sprite, layer) | |
sprite.add_internal(self) | |
def draw(self, camera_object, surface, parallax=0, return_dirty_rects=1): | |
"""Do not use this method in production. | |
It is used by Camera2 objects to draw sprites. | |
""" | |
set_clip = surface.set_clip | |
spritelist = self._spritelist | |
screenrect = camera_object._screenrect | |
set_clip(screenrect) # clipping to restrict blitting to Camera2 screenrect area | |
colliderect = Rect.colliderect # speedup | |
spritedict = self.spritedict | |
surface_blit = surface.blit | |
union = Rect.union # speedup | |
dirty = self.lostsprites | |
self.lostsprites = [] | |
dirty_append = dirty.append | |
init_rect = self._init_rect | |
X = camera_object._xfloat # float Camera2 world-coordinates | |
Y = camera_object._yfloat | |
VIEW_X, VIEW_Y = screenrect.topleft # integer Camera2 screen coordinates (used as an offset for blitting) | |
view_distance = camera_object._view_distance # Camera2 maximum view distance | |
if parallax: | |
if return_dirty_rects: | |
for spr in spritelist: | |
spr_rect = spr.rect | |
spr_x, spr_y = spr_rect.topleft | |
parallax_ratio = 1.0-abs(spr.distance/view_distance) # percentage to multiply sprite displacement by, the higher the sprite distance, the lower the displacement. | |
blitpos = (VIEW_X+(clamp(spr_x-(parallax_ratio)*(X), RECT_INT_MIN, RECT_INT_MAX)), # sprite displacement formula | |
VIEW_Y+(clamp(spr_y-(parallax_ratio)*(Y), RECT_INT_MIN, RECT_INT_MAX))) # clamping necessary to prevent potential errors with blitting coordinates (they stop at 2**16, 16 bit ints) | |
if colliderect(screenrect, (blitpos, spr_rect.size)): # check if displaced sprite is contained in the Camera2's screen view, to prevent excessive blit calls. | |
rec = spritedict[spr] | |
newrect = surface_blit(spr.image, blitpos) | |
if rec is init_rect: | |
dirty_append(newrect) | |
else: | |
if colliderect(newrect, rec): | |
dirty_append(union(newrect, rec)) | |
else: | |
dirty_append(newrect) | |
dirty_append(rec) | |
spritedict[spr] = newrect | |
else: | |
for spr in spritelist: | |
spr_rect = spr.rect | |
spr_x, spr_y = spr_rect.topleft | |
parallax_ratio = 1.0-abs(spr.distance/view_distance) | |
blitpos = (VIEW_X+(clamp(spr_x-(parallax_ratio)*(X), RECT_INT_MIN, RECT_INT_MAX)), VIEW_Y+(clamp(spr_y-(parallax_ratio)*(Y), RECT_INT_MIN, RECT_INT_MAX))) | |
if colliderect(screenrect, (blitpos, spr_rect.size)): | |
spritedict[spr] = surface_blit(spr.image, blitpos) | |
elif return_dirty_rects: | |
for spr in spritelist: | |
spr_rect = spr.rect | |
spr_x, spr_y = spr_rect.topleft | |
blitpos = (VIEW_X+spr_x-X, VIEW_Y+spr_y-Y) # simplified displacement formula, without parallax | |
if colliderect(screenrect, (blitpos, spr_rect.size)): | |
rec = spritedict[spr] | |
newrect = surface_blit(spr.image, blitpos) | |
if rec is init_rect: | |
dirty_append(newrect) | |
else: | |
if colliderect(newrect, rec): | |
dirty_append(union(newrect, rec)) | |
else: | |
dirty_append(newrect) | |
dirty_append(rec) | |
spritedict[spr] = newrect | |
else: # no need to return dirty rects if not needed | |
for spr in spritelist: | |
spr_rect = spr.rect | |
spr_x, spr_y = spr_rect.topleft | |
blitpos = (VIEW_X+spr_x-X, VIEW_Y+spr_y-Y) | |
if colliderect(screenrect, (blitpos, spr_rect.size)): | |
spritedict[spr] = surface_blit(spr.image, blitpos) | |
set_clip(None) # remove clipping | |
return dirty if return_dirty_rects else None | |
class Camera2(): | |
def __init__(self, area, view=None, view_distance=1000): | |
"""2D sprite camera class, with parallax support and rect-like behavior.""" | |
self.RETURN_POSITION_AS_FLOATS = 0 # for returning float-coordinates when preferred | |
self._anchor_point = (0,0) | |
self._view_distance = view_distance | |
self.view = CameraView() if view is None else view | |
if not isinstance(self.view, CameraView): | |
t = TypeError("view parameter must be a CameraView object or None") | |
raise t | |
self._area = list(area) # Camera2 view area in 2D world space | |
self._xfloat = self._yfloat = 0.0 # floating point coordinates, for more precise movement | |
self._screenrect = Rect(area) # area on the screen that the camera blits to | |
self.dx = 0 | |
self.dy = 0 | |
def draw(self, surface, parallax=False, return_dirty_rects=True): | |
return self.view.draw(self, surface, parallax=parallax, return_dirty_rects=return_dirty_rects) | |
def _updatepos(self): | |
self._xfloat += self.dx | |
self._yfloat += self.dy | |
self._area[:2] = int(self._xfloat), int(self._yfloat) | |
@property | |
def screenrect(self): | |
""" Camera 2 area on the screen (as a rect copy) """ | |
return self._screenrect.copy() | |
@screenrect.setter | |
def screenrect(self, rect): | |
self._screenrect = r = Rect(rect) | |
self._area[2:] = r.size | |
def slide_to(self, destination, dt, speed_factor=0.5, anchor_point=None): | |
"""slide a Camera2 object to a certain destination smoothly.""" | |
if anchor_point is None: | |
anchor_point = (0,0) | |
else: | |
anchor_point = tuple(anchor_point) | |
fac = clamp(speed_factor*dt) | |
self.dx = (destination[0]-self._xfloat-anchor_point[0])*fac # Modified Lerp formula | |
self.dy = (destination[1]-self._yfloat-anchor_point[1])*fac | |
self._updatepos() | |
def _g_anchor_p(self): | |
return self._anchor_point | |
def _s_anchor_p(self, v): | |
w, h = self._screenrect.size | |
try: | |
self._anchor_point = (clamp(v[0]+0, 0, w), clamp(v[0]+0, 0, h)) | |
return | |
except (TypeError, ValueError, IndexError): | |
pass | |
p = pygame.error("invalid assignment for Camera2 anchor point") | |
raise p | |
def _g_w(self): | |
return self._area[2] | |
def _s_w(self, v): | |
try: | |
self._area[2] = self._screenrect.w = int(v) | |
return | |
except ValueError: | |
pass | |
p = pygame.error("invalid assignment for Camera2 width") | |
raise p | |
def _g_h(self): | |
return self._area[3] | |
def _s_h(self, v): | |
try: | |
self._area[3] = self._screenrect.h = int(v) | |
return | |
except ValueError: | |
pass | |
p = pygame.error("invalid assignment for Camera2 height") | |
raise p | |
def _g_sz(self): | |
area = self._area | |
return (area[2], area[3]) | |
def _s_sz(self, v): | |
try: | |
self._area[2:] = self._screenrect.size = ( int(v[0]), int(v[1]) ) | |
return | |
except (ValueError, TypeError): | |
pass | |
p = pygame.error("invalid assignment for Camera2 size") | |
raise p | |
def _g_x(self): | |
return self._xfloat if self.RETURN_POSITION_AS_FLOATS else self._area[0] | |
def _s_x(self, v): | |
try: | |
self.dx = v-self._xfloat | |
self.dy = 0 | |
self._updatepos() | |
return | |
except (TypeError, ValueError): | |
pass | |
p = pygame.error("invalid assignment for Camera2 coordinate") | |
raise p | |
def _g_y(self): | |
return self._yfloat if self.RETURN_POSITION_AS_FLOATS else self._area[1] | |
def _s_y(self, v): | |
try: | |
self.dy = v-self._yfloat | |
self.dx = 0 | |
self._updatepos() | |
return | |
except (TypeError, ValueError): | |
pass | |
p = pygame.error("invalid assignment for Camera2 coordinate") | |
raise p | |
def _g_xy(self): | |
return (self._xfloat, self._yfloat) if self.RETURN_POSITION_AS_FLOATS else (self._area[0], self._area[1]) | |
def _g_yx(self): | |
return (self._yfloat, self._xfloat) if self.RETURN_POSITION_AS_FLOATS else (self._area[1], self._area[0]) | |
def _s_yx(self, v): | |
try: | |
self.dx = v[1]-self._xfloat | |
self.dy = v[0]-self._yfloat | |
self._updatepos() | |
return | |
except (TypeError, ValueError, IndexError): | |
pass | |
p = pygame.error("invalid assignment for Camera2 coordinates") | |
raise p | |
def _s_xy(self, v): | |
try: | |
self.dx = v[0]-self._xfloat | |
self.dy = v[1]-self._yfloat | |
self._updatepos() | |
return | |
except (TypeError, ValueError, IndexError): | |
pass | |
p = pygame.error("invalid assignment for Camera2 coordinates") | |
raise p | |
def _g_ct(self): | |
area = self._area | |
return area[0]+area[2]//2, area[1]+area[3]//2 | |
def _s_ct(self, v): | |
try: | |
area = self._area | |
#ct = (area[0]+area[2]//2, area[1]+area[3]//2) | |
self.dx = v[0]-(area[0]+area[2]//2) | |
self.dy = v[1]-(area[1]+area[3]//2) | |
self._updatepos() | |
return | |
except (TypeError, ValueError, IndexError): | |
pass | |
p = pygame.error("invalid assignment for Camera2 center") | |
raise p | |
def _g_r(self): | |
area = self._area | |
return area[0]+area[2] | |
def _s_r(self, v): | |
try: | |
area = self._area | |
self.dx = v-(area[0]+area[2]) | |
self.dy = 0 | |
self._updatepos() | |
return | |
except (TypeError, ValueError): | |
pass | |
p = pygame.error("invalid assignment for Camera2 coordinate") | |
raise p | |
def _g_b(self): | |
area = self._area | |
return area[1]+area[3] | |
def _s_b(self, v): | |
try: | |
area = self._area | |
self.dy = v-(area[1]+area[3]) | |
self.dx = 0 | |
self._updatepos() | |
return | |
except (TypeError, ValueError): | |
pass | |
p = pygame.error("invalid assignment for Camera2 coordinate") | |
raise p | |
def _g_tr(self): | |
area = self._area | |
return area[0]+area[2], area[1] | |
def _s_tr(self, v): | |
try: | |
area = self._area | |
self.dx = v[0]-(area[0]+area[2]) | |
self.dy = v[1]-(area[1]) | |
self._updatepos() | |
return | |
except (TypeError, ValueError): | |
pass | |
p = pygame.error("invalid assignment for Camera2 coordinate") | |
raise p | |
def _g_bl(self): | |
area = self._area | |
return area[0], area[1]+area[3] | |
def _s_bl(self, v): | |
try: | |
area= self._area | |
self.dx = v[0]-(area[0]) | |
self.dy = v[1]-(area[1]+area[3]) | |
self._updatepos() | |
return | |
except (TypeError, ValueError): | |
pass | |
p = pygame.error("invalid assignment for Camera2 coordinate") | |
raise p | |
def _g_br(self): | |
area = self._area | |
return area[0]+area[2], area[1]+area[3] | |
def _s_br(self, v): | |
try: | |
area = self._area | |
self.dx = v[0]-(area[0]+area[2]) | |
self.dy = v[1]-(area[1]+area[3]) | |
self._updatepos() | |
return | |
except (TypeError, ValueError): | |
pass | |
p = pygame.error("invalid assignment for Camera2 coordinate") | |
raise p | |
def _g_mt(self): | |
area = self._area | |
return area[0]+area[2]//2, area[1] | |
def _s_mt(self, v): | |
try: | |
area = self._area | |
self.dx = v[0]-(area[0]+area[2]//2) | |
self.dy = v[1]-(area[1]) | |
self._updatepos() | |
return | |
except (TypeError, ValueError): | |
pass | |
p = pygame.error("invalid assignment for Camera2 coordinate") | |
raise p | |
def _g_ml(self): | |
area = self._area | |
return area[0], area[1]+area[3]//2 | |
def _s_ml(self, v): | |
try: | |
area = self._area | |
self.dx = v[0]-(area[0]) | |
self.dy = v[1]-(area[1]+area[3]//2) | |
self._updatepos() | |
return | |
except (TypeError, ValueError): | |
pass | |
p = pygame.error("invalid assignment for Camera2 coordinate") | |
raise p | |
def _g_mb(self): | |
area = self._area | |
return area[0]+area[2]//2, area[1]+area[3] | |
def _s_mb(self, v): | |
try: | |
#mb = self._g_mb() | |
area = self._area | |
self.dx = v[0]-(area[0]+area[2]//2) | |
self.dy = v[1]-(area[1]+area[3]) | |
self._updatepos() | |
return | |
except (TypeError, ValueError): | |
pass | |
p = pygame.error("invalid assignment for Camera2 coordinate") | |
raise p | |
def _g_mr(self): | |
area = self._area | |
return area[0]+area[2], area[1]+area[3]//2 | |
def _s_mr(self, v): | |
try: | |
#mr = self._g_mr() | |
area = self._area | |
self.dx = v[0]-(area[0]+area[2]) | |
self.dy = v[1]-(area[1]+area[3]//2) | |
self._updatepos() | |
return | |
except (TypeError, ValueError): | |
pass | |
p = pygame.error("invalid assignment for Camera2 coordinate") | |
raise p | |
anchor = property(_g_anchor_p, _s_anchor_p) # special anchor point for (.goto()) method | |
topleft = xy = pos = property(_g_xy, _s_xy) | |
topright = property(_g_tr, _s_tr) | |
midtop = property(_g_mt, _s_mt) | |
midleft = property(_g_ml, _s_ml) | |
midbottom = property(_g_mb, _s_mb) | |
midright = property(_g_mr, _s_mr) | |
bottomleft = property(_g_bl, _s_bl) | |
bottomright = property(_g_br, _s_br) | |
center = middle = property(_g_ct, _s_ct) | |
yx = property(_g_yx, _s_yx) | |
x = left = property(_g_x, _s_x) | |
y = top = property(_g_x, _s_x) | |
right = property(_g_r, _s_r) | |
bottom = property(_g_b, _s_b) | |
width = w = property(_g_w, _s_w) | |
height = h = property(_g_h, _s_h) | |
size = property(_g_sz, _s_sz) | |
def move(self, dx_or_dy=None, dy=None): # (dx_or_dy=None, dy=None) this allows the method to support iterables by indexing, as well as two numbers as input. | |
if dx_or_dy is not None: | |
if dy is not None: | |
try: | |
self.dx=dx_or_dy+0.0; self.dy=dy+0.0 | |
return self._updatepos() | |
except TypeError: | |
pass | |
else: | |
try: | |
self.dx = dx_or_dy[0]+0.0; self.dy = dx_or_dy[1]+0.0 | |
return self._updatepos() | |
except TypeError: | |
try: | |
self.dx = self.dy = dx_or_dy+0.0 | |
return self._updatepos() | |
except TypeError: | |
pass | |
except IndexError: | |
pass | |
else: | |
return self._updatepos() | |
t = TypeError("invalid coordinates given as input: ("+repr(dx_or_dy)+", "+repr(dy)+"). Inputs must be two real numbers, or a tuple, list or vector object with 2 coordinates.") | |
raise t | |
def goto(self, x_or_xy=None, y=None): | |
"""Move this Camera2 object to a certain location, based on its anchor.""" | |
if x_or_xy is not None: | |
if y is not None: | |
try: | |
self.dx=(x_or_xy+0.0)-self._xfloat-self._anchor_point[0]; self.dy=(y+0.0)-self._yfloat-self._anchor_point[1] | |
return self._updatepos() | |
except TypeError: | |
pass | |
else: | |
try: | |
self.dx=(x_or_xy[0]+0.0)-self._xfloat-self._anchor_point[0]; self.dy=(x_or_xy[1]+0.0)-self._yfloat-self._anchor_point[1] | |
return self._updatepos() | |
except TypeError: | |
try: | |
self.dx=(x_or_xy+0.0)-self._xfloat-self._anchor_point[0]; self.dy=(x_or_xy+0.0)-self._yfloat-self._anchor_point[1] | |
return self._updatepos() | |
except TypeError: | |
pass | |
except IndexError: | |
pass | |
else: | |
return | |
t = TypeError("invalid coordinates given as input: ("+repr(x_or_xy)+", "+repr(y)+"). Inputs must be two real numbers, or a tuple, list or vector object with 2 coordinates.") | |
raise t | |
def teleport(self, x_or_xy=None, y=None): | |
old_dx, old_dy = self.dx, self.dy | |
passed = False | |
if x_or_xy is not None: | |
if y is not None: | |
try: | |
self.dx=(x_or_xy+0.0)-self._xfloat-self._anchor_point[0]; self.dy=(y+0.0)-self._yfloat-self._anchor_point[1] | |
self._updatepos() | |
passed = True | |
except TypeError: | |
pass | |
else: | |
try: | |
self.dx=(x_or_xy[0]+0.0)-self._xfloat-self._anchor_point[0]; self.dy=(x_or_xy[1]+0.0)-self._yfloat-self._anchor_point[1] | |
self._updatepos() | |
passed = True | |
except TypeError: | |
try: | |
self.dx=(x_or_xy+0.0)-self._xfloat-self._anchor_point[0]; self.dy=(x_or_xy+0.0)-self._yfloat-self._anchor_point[1] | |
self._updatepos() | |
passed = True | |
except TypeError: | |
pass | |
except IndexError: | |
pass | |
else: | |
return | |
self.dx, self.dy = old_dx, old_dy | |
if not passed: | |
t = TypeError("invalid coordinates given as input: ("+repr(x_or_xy)+", "+repr(y)+"). Inputs must be two real numbers, or a tuple, list or vector object with 2 coordinates.") | |
raise t | |
tp = teleport | |
def get_pos(self, precise=False): | |
"""Get the world postion of this Camera2 object.""" | |
area = self._area | |
return (self._xfloat, self._yfloat) if precise else (area[0], area[1]) | |
def reset_pos(self): | |
"""Reset the world postion of this Camera2 object.""" | |
area = self._area | |
area[:] = (0.0, 0.0, area[3], area[4]) | |
self._xfloat = self._yfloat = 0.0 | |
def get_area(self): | |
"""Return the area of this Camera2 object in world-space.""" | |
area = self._area | |
return (area[0], area[1], area[2], area[3]) | |
def set_size(self, x_or_xy=None, y=None): | |
"""Set the view size of this Camera2 object.""" | |
passed = False | |
if x_or_xy is not None: | |
if y is not None: | |
try: | |
self._area[2:] = self._screenrect.size = x_or_xy+0, y+0 | |
passed = True | |
except TypeError: | |
pass | |
else: | |
try: | |
self._area[2:] = self._screenrect.size = x_or_xy[0]+0, x_or_xy[1]+0 | |
passed = True | |
except TypeError: | |
try: | |
self._area[2:] = self._screenrect.size = x_or_xy.x+0, x_or_xy.y+0 | |
passed = True | |
except AttributeError: | |
try: | |
self._area[2:] = self._screenrect.size = (x_or_xy+0, x_or_xy+0) | |
passed = True | |
except Exception: | |
pass | |
except IndexError: | |
pass | |
else: | |
return | |
if not passed: | |
t = TypeError("invalid coordinates given as input: ("+repr(x_or_xy)+", "+repr(y)+"). Inputs must be two real numbers, or a tuple, list or vector object with 2 coordinates.") | |
raise t | |
@property | |
def view_distance(self): | |
return self._view_distance | |
@view_distance.setter | |
def view_distance(self, d): | |
try: | |
self._view_distance = max2(abs(d+0.0), 1) | |
except TypeError as t: | |
t.args = ("invalid input for 'view_distance' parameter, it must be a positive real number",) | |
raise t from None | |
def __str__(self): | |
return "<(Camera2 | area: {0}, at [{1:.5f}, {2:.5f}])>".format(tuple(self._screenrect), self._xfloat, self._yfloat) | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import pygame as pg | |
from pygame.locals import* | |
from CameraLib import Camera2 | |
from pygame import Surface | |
from sys import exit | |
from random import randrange | |
pg.init() | |
RED = (255,0,0) | |
BLUE = (0, 0, 255) | |
GREY = (127, 127, 127) | |
class GameObj(pg.sprite.Sprite): | |
def __init__(self, img, pos=(0,0), distance=0): | |
pg.sprite.Sprite.__init__(self) | |
self.image = img | |
self.rect = img.get_rect() | |
self.rect.topleft = pos | |
self.distance = distance | |
self.dx = self.dy = 0 | |
def update(self, *args, **kwargs): | |
self.rect.move_ip(self.dx, self.dy) | |
WIN = pg.display.set_mode((1600, 900)) | |
Cam = Camera2((0, 0, 1600, 900), view_distance=1000) | |
player = GameObj(Surface((300, 300))) | |
Cam.center = player.rect.center = (0,0) | |
player.image.fill(BLUE) | |
circle_surfs = [Surface((randrange(50, 500),)*2, SRCALPHA) for i in range(100)] | |
print(circle_surfs) | |
circle_sprites = [] | |
circle_surfs.sort(key=lambda x: x.get_size()) # sort surfaces by size | |
circle_surfs_len = 100 | |
for i in range(100): | |
surf = circle_surfs[i] | |
pg.draw.circle(surf, (randrange(50, 200), randrange(50, 200), randrange(50, 200)), center=(surf.get_width()//2, surf.get_height()//2), radius=surf.get_width()//2) | |
obj = GameObj(surf, distance=((100-i)/100)*1000) # this places the sprites randomly, with their (.distance) depending on the index of their surface in the list | |
obj.rect.center = (randrange(0, 501), randrange(0, 501)) | |
circle_sprites.append( obj ) | |
Cam.view.add(circle_sprites, player) | |
clock = pg.time.Clock() | |
running = 1 | |
while running: | |
for event in pg.event.get(): | |
if event.type == QUIT: | |
running = 0 | |
elif event.type == KEYDOWN: | |
if event.key == K_d: | |
player.dx = 10 | |
elif event.key == K_a: | |
player.dx = -10 | |
if event.key == K_s: | |
player.dy = 10 | |
elif event.key == K_w: | |
player.dy = -10 | |
elif event.type == KEYUP: | |
if event.key == K_d: | |
player.dx = 0 | |
elif event.key == K_a: | |
player.dx = 0 | |
if event.key == K_s: | |
player.dy = 0 | |
elif event.key == K_w: | |
player.dy = 0 | |
WIN.fill(GREY) | |
player.update() | |
Cam.center = player.rect.center | |
#Cam.slide_to(player.rect.center, 1/(clock.get_fps()+1e-6), speed_factor=10, anchor_point=Cam.screenrect.center) | |
Cam.draw(WIN, parallax=1, return_dirty_rects=1) | |
clock.tick(60) | |
pg.display.update() | |
pg.quit() | |
exit() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import pygame as pg | |
from pygame.locals import* | |
from CameraLib import Camera2 | |
from pygame import Surface | |
from sys import exit | |
from random import randrange | |
pg.init() | |
RED = (255,0,0) | |
BLUE = (0, 0, 255) | |
GREY = (127, 127, 127) | |
class GameObj(pg.sprite.Sprite): | |
def __init__(self, img, pos=(0,0), distance=0): | |
pg.sprite.Sprite.__init__(self) | |
self.image = img | |
self.rect = img.get_rect() | |
self.rect.topleft = pos | |
self.distance = distance | |
self.dx = self.dy = 0 | |
def update(self, *args, **kwargs): | |
self.rect.move_ip(self.dx, self.dy) | |
WIN = pg.display.set_mode((1600, 900)) | |
Cam = Camera2((0, 0, 800, 900), view_distance=1000) | |
Cam2 = Camera2((800, 0, 800, 900), view=Cam.view, view_distance=1000) | |
player = GameObj(Surface((300, 300))) | |
Cam2.center = Cam.center = player.rect.center = (0,0) | |
player.image.fill(BLUE) | |
circle_surfs = [Surface((randrange(50, 500),)*2, SRCALPHA) for i in range(50)] | |
print(circle_surfs) | |
circle_sprites = [] | |
circle_surfs.sort(key=lambda x: x.get_size()) # sort surfaces by size | |
circle_surfs_len = 50 | |
for i in range(50): | |
surf = circle_surfs[i] | |
pg.draw.circle(surf, (randrange(50, 200), randrange(50, 200), randrange(50, 200)), center=(surf.get_width()//2, surf.get_height()//2), radius=surf.get_width()//2) | |
obj = GameObj(surf, distance=((50-i)/50)*1000) # this places the sprites randomly, with their (.distance) depending on the index of their surface in the list | |
obj.rect.center = (randrange(0, 501), randrange(0, 501)) | |
circle_sprites.append( obj ) | |
Cam.view.add(circle_sprites, player) | |
Cam2.view = Cam.view | |
clock = pg.time.Clock() | |
running = 1 | |
while running: | |
for event in pg.event.get(): | |
if event.type == QUIT: | |
running = 0 | |
elif event.type == KEYDOWN: | |
if event.key == K_d: | |
player.dx = 10 | |
elif event.key == K_a: | |
player.dx = -10 | |
if event.key == K_s: | |
player.dy = 10 | |
elif event.key == K_w: | |
player.dy = -10 | |
elif event.type == KEYUP: | |
if event.key == K_d: | |
player.dx = 0 | |
elif event.key == K_a: | |
player.dx = 0 | |
if event.key == K_s: | |
player.dy = 0 | |
elif event.key == K_w: | |
player.dy = 0 | |
WIN.fill(GREY) | |
player.update() | |
Cam.center = player.rect.center | |
Cam2.slide_to(player.rect.center, 1/(clock.get_fps()+1e-6), speed_factor=10, anchor_point=(Cam2.w//2, Cam2.h//2)) # Note: for Cam2, the origin is at the topleft corner of its area (800, 0) | |
Cam.draw(WIN, parallax=1, return_dirty_rects=0) | |
Cam2.draw(WIN, parallax=1, return_dirty_rects=0) | |
clock.tick(60) | |
pg.display.update(Rect(0,0,500,500)) | |
pg.quit() | |
exit() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment