5

From the pymunk examples I've seen that there's a difference between the pymunk coordinates and pygame coordinates. Also, that pymunk is meant just for the 2D physics, while pygame is for rendering objects/sprites on the screen.

So when searching for how to build an environment where the camera follows the player, people (including me) end up getting confused. I've seen the examples here, here, here and here (even surprised that nobody answered this), but given the number of questions related to the same topic being asked repeatedly, I honestly feel the answers do not adequately explain the concept and request that the simplest possible example be shown to the community, where all the code is explained with comments.

I've worked in 3D environments like OGRE and OSG where the camera was a proper concept that could be defined with a view frustum, but I'm surprised the 2D world does not have a pre-defined function for it. So:

If not in the official tutorials of pymunk or pygame, at least could a simple example be provided (with a pymunk body as the player and few pymunk bodies in the world) as an answer here, where a player moves around in a 2D pymunk+pygame world and the camera follows the player?

Nav
  • 19,885
  • 27
  • 92
  • 135

1 Answers1

9

OK, I'll try to make this simple (I assume basic pygame knowledge).

First, let's start with something basic. A little sprite that you can move around the world:

import pygame
import random

class Player(pygame.sprite.Sprite):
    def __init__(self):
        super().__init__()
        self.image = pygame.Surface((32, 32))
        self.image.fill(pygame.Color('dodgerblue'))
        self.rect = self.image.get_rect()
        self.pos = pygame.Vector2((100, 200))

    def update(self, events, dt):
        pressed = pygame.key.get_pressed()
        move = pygame.Vector2((0, 0))
        if pressed[pygame.K_w]: move += (0, -1)
        if pressed[pygame.K_a]: move += (-1, 0)
        if pressed[pygame.K_s]: move += (0, 1)
        if pressed[pygame.K_d]: move += (1, 0)
        if move.length() > 0: move.normalize_ip()
        self.pos += move*(dt/5)
        self.rect.center = self.pos

def main():
    pygame.init()
    screen = pygame.display.set_mode((500, 500))
    clock = pygame.time.Clock()
    dt = 0
    player = Player()
    sprites = pygame.sprite.Group(player)
    background = screen.copy()
    background.fill((30, 30, 30))
    for _ in range(1000):
        x, y = random.randint(0, 1000), random.randint(0, 1000)
        pygame.draw.rect(background, pygame.Color('green'), (x, y, 2, 2))

    while True:
        events = pygame.event.get()
        for e in events:
            if e.type == pygame.QUIT:
                return
        sprites.update(events, dt)
        screen.blit(background, (0, 0))
        sprites.draw(screen)
        pygame.display.update()
        dt = clock.tick(60)

if __name__ == '__main__':
    main()

enter image description here

Nothing crazy so far.


So, what is a "camera"? It's just a x and an y value we use to move the entire "world" (e.g. everything that is not UI). It's an abstraction between the coordinates of our game objects and the screen.

In our example above, when a game object (the player, or the background) wants to be drawn at position (x, y), we draw them at the screen at this very position.

Now, if we want to move around a "camera", we simply create another x, y-pair, and add this to the game object's coordinates to determine the actual position on the screen. We start to distinguish between world coordinates (what the game logic thinks where the position of an object is) and the screen coordinates (the actual position of an object on the screen).

Here's our example with a "camera" ("camera" in quotes) because it's really just two values:

import pygame
import random

class Player(pygame.sprite.Sprite):
    def __init__(self):
        super().__init__()
        self.image = pygame.Surface((32, 32))
        self.image.fill(pygame.Color('dodgerblue'))
        self.rect = self.image.get_rect()
        self.pos = pygame.Vector2((100, 200))

    def update(self, events, dt):
        pressed = pygame.key.get_pressed()
        move = pygame.Vector2((0, 0))
        if pressed[pygame.K_w]: move += (0, -1)
        if pressed[pygame.K_a]: move += (-1, 0)
        if pressed[pygame.K_s]: move += (0, 1)
        if pressed[pygame.K_d]: move += (1, 0)
        if move.length() > 0: move.normalize_ip()
        self.pos += move*(dt/5)
        self.rect.center = self.pos

def main():
    pygame.init()
    screen = pygame.display.set_mode((500, 500))
    clock = pygame.time.Clock()
    dt = 0
    player = Player()
    sprites = pygame.sprite.Group(player)
    # the "world" is now bigger than the screen
    # so we actually have anything to move the camera to
    background = pygame.Surface((1500, 1500))
    background.fill((30, 30, 30))

    # a camera is just two values: x and y
    # we use a vector here because it's easier to handle than a tuple
    camera = pygame.Vector2((0, 0))

    for _ in range(3000):
        x, y = random.randint(0, 1000), random.randint(0, 1000)
        pygame.draw.rect(background, pygame.Color('green'), (x, y, 2, 2))

    while True:
        events = pygame.event.get()
        for e in events:
            if e.type == pygame.QUIT:
                return

        # copy/paste because I'm lazy
        # just move the camera around
        pressed = pygame.key.get_pressed()
        camera_move = pygame.Vector2()
        if pressed[pygame.K_UP]: camera_move += (0, 1)
        if pressed[pygame.K_LEFT]: camera_move += (1, 0)
        if pressed[pygame.K_DOWN]: camera_move += (0, -1)
        if pressed[pygame.K_RIGHT]: camera_move += (-1, 0)
        if camera_move.length() > 0: camera_move.normalize_ip()
        camera += camera_move*(dt/5)

        sprites.update(events, dt)

        # before drawing, we shift everything by the camera's x and y values
        screen.blit(background, camera)
        for s in sprites:
            screen.blit(s.image, s.rect.move(*camera))

        pygame.display.update()
        dt = clock.tick(60)

if __name__ == '__main__':
    main()

enter image description here

Now you can move the camera with the arrow keys.

That's it. We just move everything a little bit before blitting it to the screen.

For a more complete example (supporting sprites, stopping at the edge of the world, smooth movement), see this question.


And for using pymunk: it just works. It's not affected by drawing stuff to another position, since it works with the world coordinates, not the screen coordinates. The only pitfall is that pymunk's y-axis is flipped compared to pygame's y-axis, but you probably know this already.

Here's an example:

import pygame
import random
import pymunk

class Player(pygame.sprite.Sprite):
    def __init__(self, space):
        super().__init__()
        self.space = space
        self.image = pygame.Surface((32, 32))
        self.image.fill(pygame.Color('dodgerblue'))
        self.rect = self.image.get_rect()
        self.pos = pygame.Vector2((100, 200))
        self.body = pymunk.Body(1,1666)
        self.body.position = self.pos
        self.poly = pymunk.Poly.create_box(self.body)
        self.space.add(self.body, self.poly)

    def update(self, events, dt):
        pressed = pygame.key.get_pressed()
        move = pygame.Vector2((0, 0))
        if pressed[pygame.K_w]: move += (0, 1)
        if pressed[pygame.K_a]: move += (-1, 0)
        if pressed[pygame.K_s]: move += (0, -1)
        if pressed[pygame.K_d]: move += (1, 0)
        if move.length() > 0: move.normalize_ip()
        self.body.apply_impulse_at_local_point(move*5)

        # if you used pymunk before, you'll probably already know
        # that you'll have to invert the y-axis to convert between
        # the pymunk and the pygame coordinates.
        self.pos = pygame.Vector2(self.body.position[0], -self.body.position[1]+500)
        self.rect.center = self.pos

def main():
    pygame.init()
    screen = pygame.display.set_mode((500, 500))
    clock = pygame.time.Clock()
    dt = 0

    space = pymunk.Space()
    space.gravity = 0,-100

    player = Player(space)
    sprites = pygame.sprite.Group(player)

    # the "world" is now bigger than the screen
    # so we actually have anything to move the camera to
    background = pygame.Surface((1500, 1500))
    background.fill((30, 30, 30))

    # a camera is just two values: x and y
    # we use a vector here because it's easier to handle than a tuple
    camera = pygame.Vector2((0, 0))

    for _ in range(3000):
        x, y = random.randint(0, 1000), random.randint(0, 1000)
        pygame.draw.rect(background, pygame.Color('green'), (x, y, 2, 2))

    while True:
        events = pygame.event.get()
        for e in events:
            if e.type == pygame.QUIT:
                return

        # copy/paste because I'm lazy
        # just move the camera around
        pressed = pygame.key.get_pressed()
        camera_move = pygame.Vector2()
        if pressed[pygame.K_UP]: camera_move += (0, 1)
        if pressed[pygame.K_LEFT]: camera_move += (1, 0)
        if pressed[pygame.K_DOWN]: camera_move += (0, -1)
        if pressed[pygame.K_RIGHT]: camera_move += (-1, 0)
        if camera_move.length() > 0: camera_move.normalize_ip()
        camera += camera_move*(dt/5)

        sprites.update(events, dt)

        # before drawing, we shift everything by the camera's x and y values
        screen.blit(background, camera)
        for s in sprites:
            screen.blit(s.image, s.rect.move(*camera))

        pygame.display.update()
        dt = clock.tick(60)
        space.step(dt/1000)

if __name__ == '__main__':
    main()

enter image description here


Note that when you use pymunk.Space.debug_draw, you won't be able to translate the world coordinates to screen coordinates, so it would be best to simply draw the pymunk stuff to another Surface, and translate that very Surface.

Here's pymunk's pygame_util_demo.py with a moving camera:

import sys

import pygame
from pygame.locals import *

import pymunk
from pymunk.vec2d import Vec2d
import pymunk.pygame_util

import shapes_for_draw_demos

def main():
    pygame.init()
    screen = pygame.display.set_mode((1000,700)) 
    pymunk_layer = pygame.Surface((1000,700))
    pymunk_layer.set_colorkey((12,12,12))
    pymunk_layer.fill((12,12,12))
    camera = pygame.Vector2((0, 0))
    clock = pygame.time.Clock()
    font = pygame.font.SysFont("Arial", 16)

    space = pymunk.Space()

    captions = shapes_for_draw_demos.fill_space(space)

    # Info
    color = pygame.color.THECOLORS["black"]

    options = pymunk.pygame_util.DrawOptions(pymunk_layer)

    while True:
        for event in pygame.event.get():
            if event.type == QUIT or \
                event.type == KEYDOWN and (event.key in [K_ESCAPE, K_q]):  
                return 
            elif event.type == KEYDOWN and event.key == K_p:
                pygame.image.save(screen, "pygame_util_demo.png")                

        # copy/paste because I'm lazy
        pressed = pygame.key.get_pressed()
        camera_move = pygame.Vector2()
        if pressed[pygame.K_UP]: camera_move += (0, 1)
        if pressed[pygame.K_LEFT]: camera_move += (1, 0)
        if pressed[pygame.K_DOWN]: camera_move += (0, -1)
        if pressed[pygame.K_RIGHT]: camera_move += (-1, 0)
        if camera_move.length() > 0: camera_move.normalize_ip()
        camera += camera_move*5

        screen.fill(pygame.color.THECOLORS["white"])
        pymunk_layer.fill((12,12,12))
        space.debug_draw(options)
        screen.blit(pymunk_layer, camera)
        screen.blit(font.render("Demo example of pygame_util.DrawOptions()", 1, color), (205, 680))
        for caption in captions:
            x, y = caption[0]
            y = 700 - y
            screen.blit(font.render(caption[1], 1, color), camera + (x,y))
        pygame.display.flip()

        clock.tick(30)

if __name__ == '__main__':
    sys.exit(main())

enter image description here

sloth
  • 99,095
  • 21
  • 171
  • 219
  • Thanks. The explanation was helpful. I think it's the verbal statement of pymunk's coordinates that would be a lot clearer to newbies if it could be shown in code where the player is a pymunk body and there are few other pymunk bodies on the screen. – Nav Mar 19 '19 at 08:25
  • @Nav I added a simple pymunk example. – sloth Mar 19 '19 at 10:23
  • Brilliant! You've shown how to control the player and camera separately. This is going to become a valuable reference for everyone. I'll try creating a tutorial based on this. Thank you. – Nav Mar 19 '19 at 17:03
  • A great answer @Sloth. Why do you divide `dt` by `5` when using it for movement? (`dt` is calculated by `clock.tick()`, and is the time used between frames, in milliseconds). – Kingsley Mar 19 '19 at 22:53
  • @Kingsley just to slow it down. Try changing the value and see the effect. Diving dt by 5 ist arbitary choosen. – sloth Mar 19 '19 at 23:39
  • @sloth: I've been trying to apply the example in my program and am still finding it hard because you've used pygame surfaces while I'm using pymunk's static and dynamic bodies (shown here https://youtu.be/jW4-BblBd_M). This is why I had highlighted in bold in my question that I was looking for an example with pymunk bodies. The problem is that pymunk uses `debug_draw` and even updating the positions of the cars and static tracks together to move them left or right on keypress (to simulate a camera) doesn't work. – Nav Mar 20 '19 at 08:22
  • @Nav I see. I didn't know about `debug_draw`. I added a possible solution to my answer. – sloth Mar 20 '19 at 08:50
  • Ok so that's the missing part to the puzzle. I **have** to use a pygame Surface. I wasn't using that until now. Thank you. – Nav Mar 20 '19 at 11:41
  • BEAUTIFUL CODE AND EXPLANATION, THIS SAVED ME!!! you are a god this is underrated – Sha7r Nov 06 '21 at 15:06