Search code examples
pymunk

The right way to create and update objects in a Pymunk camera windowed world?


I asked about how to have a camera window view of a Pymunk+Pygame world and although I received a generously explained answer, I'm not convinced, because the person uses a Pygame Surface to blit. The API says a Surface is meant to represent an image and from both a processing and rendering point of view I felt it's an inefficient way to render Pymunk objects, especially when they are kinematic.

So I tried modifying one of the Pymunk examples (I've commented "Nav added" wherever I've made changes) to move around the balls and static lines when arrow keys are pressed. It creates a camera-kind-of effect. But when I use the arrow keys to move the object to the right, even the mouse coordinates seem to move along.

enter image description here
I thought the problem was with line p = event.pos[X]+cameraX, flipy(event.pos[Y])+cameraY, but even after changing it to p = event.pos[X], flipy(event.pos[Y]), the problem persists.

"""This example lets you dynamically create static walls and dynamic balls

"""
__docformat__ = "reStructuredText"

import pygame
from pygame.locals import *
from pygame.color import *

import pymunk
from pymunk import Vec2d


X,Y = 0,1
### Physics collision types
COLLTYPE_DEFAULT = 0
COLLTYPE_MOUSE = 1
COLLTYPE_BALL = 2

def flipy(y):
    """Small hack to convert chipmunk physics to pygame coordinates"""
    return -y+600

def mouse_coll_func(arbiter, space, data):
    """Simple callback that increases the radius of circles touching the mouse"""
    s1,s2 = arbiter.shapes
    s2.unsafe_set_radius(s2.radius + 0.15)
    return False

def main():            
    pygame.init()
    screen = pygame.display.set_mode((600, 600))
    clock = pygame.time.Clock()
    running = True

    # Camera offsets (Nav added)
    cameraX = 0
    cameraY = 0

    ### Physics stuff
    space = pymunk.Space()
    space.gravity = 0.0, -900.0

    ## Balls
    balls = []

    ### Mouse
    mouse_body = pymunk.Body(body_type=pymunk.Body.KINEMATIC)
    mouse_shape = pymunk.Circle(mouse_body, 3, (0,0))
    mouse_shape.collision_type = COLLTYPE_MOUSE
    space.add(mouse_shape)

    space.add_collision_handler(COLLTYPE_MOUSE, COLLTYPE_BALL).pre_solve=mouse_coll_func   

    ### Static line
    line_point1 = None
    static_lines = []
    run_physics = True

    while running:
        for event in pygame.event.get():
            if event.type == QUIT:
                running = False
            elif event.type == KEYDOWN and event.key == K_ESCAPE:
                running = False
            elif event.type == KEYDOWN and event.key == K_p:
                pygame.image.save(screen, "balls_and_lines.png")
            elif event.type == MOUSEBUTTONDOWN and event.button == 1:
                p = event.pos[X]+cameraX, flipy(event.pos[Y])+cameraY#Nav added
                body = pymunk.Body(10, 100)
                body.position = p
                shape = pymunk.Circle(body, 10, (0,0))
                shape.friction = 0.5
                shape.collision_type = COLLTYPE_BALL
                space.add(body, shape)
                balls.append(shape)
            #Nav added key detection event
            if event.type == KEYDOWN:
                if event.key == K_UP:
                    cameraY -= 10
                    print("x:"+str(cameraX)+" y:"+str(cameraY))
                if event.key == K_LEFT: 
                    cameraX -= 10
                if event.key == K_DOWN:
                    cameraY += 10
                if event.key == K_RIGHT:
                    cameraX += 10                

            elif event.type == MOUSEBUTTONDOWN and event.button == 3: 
                if line_point1 is None:
                    line_point1 = Vec2d(event.pos[X]+cameraX, flipy(event.pos[Y])+cameraY)#Nav added
            elif event.type == MOUSEBUTTONUP and event.button == 3: 
                if line_point1 is not None:                    
                    line_point2 = Vec2d(event.pos[X]+cameraX, flipy(event.pos[Y])+cameraY)#Nav added
                    body = pymunk.Body(body_type=pymunk.Body.STATIC)
                    shape= pymunk.Segment(body, line_point1, line_point2, 0.0)
                    shape.friction = 0.99
                    space.add(shape)
                    static_lines.append(shape)
                    line_point1 = None

            elif event.type == KEYDOWN and event.key == K_SPACE:    
                run_physics = not run_physics

        p = pygame.mouse.get_pos()
        mouse_pos = Vec2d(p[X]+cameraX, flipy(p[Y])+cameraY)#Nav added
        mouse_body.position = mouse_pos


        if pygame.key.get_mods() & KMOD_SHIFT and pygame.mouse.get_pressed()[0]:
            body = pymunk.Body(10, 10)
            body.position = mouse_pos
            shape = pymunk.Circle(body, 10, (0,0))
            shape.collision_type = COLLTYPE_BALL
            space.add(body, shape)
            balls.append(shape)

        ### Update physics
        if run_physics:
            dt = 1.0/60.0
            for x in range(1):
                space.step(dt)

        ### Draw stuff
        screen.fill(THECOLORS["white"])

        # Display some text
        font = pygame.font.Font(None, 16)
        text = """LMB: Create ball
LMB + Shift: Create many balls
RMB: Drag to create wall, release to finish
Space: Pause physics simulation"""
        y = 5
        for line in text.splitlines():
            text = font.render(line, 1,THECOLORS["black"])
            screen.blit(text, (5,y))
            y += 10

        for ball in balls:           
            r = ball.radius
            v = ball.body.position
            rot = ball.body.rotation_vector
            p = int(v.x)+cameraX, int(flipy(v.y))+cameraY#Nav added
            p2 = Vec2d(rot.x, -rot.y) * r * 0.9
            pygame.draw.circle(screen, THECOLORS["blue"], p, int(r), 2)
            pygame.draw.line(screen, THECOLORS["red"], p, p+p2)

        if line_point1 is not None:
            p1 = line_point1.x+cameraX, flipy(line_point1.y)+cameraY#Nav added
            p2 = mouse_pos.x+cameraX, flipy(mouse_pos.y)+cameraY#Nav added
            pygame.draw.lines(screen, THECOLORS["black"], False, [p1,p2])

        for line in static_lines:
            body = line.body

            pv1 = body.position + line.a.rotated(body.angle)
            pv2 = body.position + line.b.rotated(body.angle)
            p1 = pv1.x+cameraX, flipy(pv1.y)+cameraY#Nav added
            p2 = pv2.x+cameraX, flipy(pv2.y)+cameraY#Nav added
            pygame.draw.lines(screen, THECOLORS["lightgray"], False, [p1,p2])

        ### Flip screen
        pygame.display.flip()
        clock.tick(50)
        pygame.display.set_caption("fps: " + str(clock.get_fps()))

if __name__ == '__main__':
    doprof = 0
    if not doprof: 
        main()
    else:
        import cProfile, pstats

        prof = cProfile.run("main()", "profile.prof")
        stats = pstats.Stats("profile.prof")
        stats.strip_dirs()
        stats.sort_stats('cumulative', 'time', 'calls')
        stats.print_stats(30)

Am I doing the right thing by moving all ball and static line positions in every frame? I assume a more efficient way would be to move all of them every frame but draw only the ones that are within screen limits?

On the other hand, in the example like the one below, the static lines are added before the run loop, and it leaves me wondering what is the right way to implement the camera in this case. Perhaps I'm left with no option other than to use Surface.

"""This example spawns (bouncing) balls randomly on a L-shape constructed of 
two segment shapes. Not interactive.
"""

__version__ = "$Id:$"
__docformat__ = "reStructuredText"

# Python imports
import random

# Library imports
import pygame
from pygame.key import *
from pygame.locals import *
from pygame.color import *

# pymunk imports
import pymunk
import pymunk.pygame_util


class BouncyBalls(object):
    """
    This class implements a simple scene in which there is a static platform (made up of a couple of lines)
    that don't move. Balls appear occasionally and drop onto the platform. They bounce around.
    """
    def __init__(self):
        # Space
        self._space = pymunk.Space()
        self._space.gravity = (0.0, -900.0)
        # Camera offsets (Nav added)
        self.cameraX = 0
        self.cameraY = 0
        # Physics
        # Time step
        self._dt = 1.0 / 60.0
        # Number of physics steps per screen frame
        self._physics_steps_per_frame = 1

        # pygame
        pygame.init()
        self._screen = pygame.display.set_mode((600, 600))
        self._clock = pygame.time.Clock()

        self._draw_options = pymunk.pygame_util.DrawOptions(self._screen)

        # Static barrier walls (lines) that the balls bounce off of
        self._add_static_scenery()

        # Balls that exist in the world
        self._balls = []

        # Execution control and time until the next ball spawns
        self._running = True
        self._ticks_to_next_ball = 10

    def run(self):
        # Main loop
        while self._running:
            # Progress time forward
            for x in range(self._physics_steps_per_frame):
                self._space.step(self._dt)

            self._process_events()
            self._update_balls()
            self._clear_screen()
            self._draw_objects()
            pygame.display.flip()
            # Delay fixed time between frames
            self._clock.tick(50)
            pygame.display.set_caption("fps: " + str(self._clock.get_fps()))

    def _add_static_scenery(self):
        static_body = self._space.static_body
        #Nav added offsets
        static_lines = [pymunk.Segment(static_body, (111.0+self.cameraX, 280.0+self.cameraY), (407.0+self.cameraX, 246.0+self.cameraY), 0.0),
                        pymunk.Segment(static_body, (407.0+self.cameraX, 246.0+self.cameraY), (407.0+self.cameraX, 343.0+self.cameraY), 0.0)]
        for line in static_lines:
            line.elasticity = 0.95
            line.friction = 0.9
        self._space.add(static_lines)

    def _process_events(self):
        for event in pygame.event.get():
            if event.type == QUIT:
                self._running = False
            elif event.type == KEYDOWN and event.key == K_ESCAPE:
                self._running = False
            elif event.type == KEYDOWN and event.key == K_p:
                pygame.image.save(self._screen, "bouncing_balls.png")

            #Nav added key detection event
            if event.type == KEYDOWN:
                if event.key == K_UP:
                    self.cameraY -= 10
                    print("x:"+str(self.cameraX)+" y:"+str(self.cameraY))
                if event.key == K_LEFT: 
                    self.cameraX -= 10
                if event.key == K_DOWN:
                    self.cameraY += 10
                if event.key == K_RIGHT:
                    self.cameraX += 10


    def _update_balls(self):
        self._ticks_to_next_ball -= 1
        if self._ticks_to_next_ball <= 0:
            self._create_ball()
            self._ticks_to_next_ball = 100
        # Remove balls that fall below 100 vertically
        balls_to_remove = [ball for ball in self._balls if ball.body.position.y+self.cameraY < 100]#Nav added offset
        for ball in balls_to_remove:
            self._space.remove(ball, ball.body)
            self._balls.remove(ball)

    def _create_ball(self):
        mass = 10
        radius = 25
        inertia = pymunk.moment_for_circle(mass, 0, radius, (0, 0))
        body = pymunk.Body(mass, inertia)
        x = random.randint(115+self.cameraX, 350+self.cameraY)#Nav added offset
        body.position = x+self.cameraX, 400+self.cameraY #Nav added offset
        shape = pymunk.Circle(body, radius, (0, 0))
        shape.elasticity = 0.95
        shape.friction = 0.9
        self._space.add(body, shape)
        self._balls.append(shape)

    def _clear_screen(self):
        self._screen.fill(THECOLORS["white"])

    def _draw_objects(self):
        self._space.debug_draw(self._draw_options)


if __name__ == '__main__':
    game = BouncyBalls()
    game.run()

In summary, I'm trying to create a much largeer world than the one shown below (a typical Mario-type world), where the rectangular area in dotted lines is visible to the user on the entire computer monitor and some status text is shown at the corners of the area. Any part of the world that's outside the dotted lines is outside the field of view of the monitor.
All I want to know is the right way to create such a camera view, add static and kinematic objects to this world and update them while the mouse clicks and keyboard inputs are relevant to the visible area of the screen. This has become confusing because Pygame and Pymunk have different ways of drawing and updating elements. A clear example would be a huge help for the community.

enter image description here

Also, if its a multiplayer game, it may be necessary to have a separate view region for the other person (spider) who is looking at the same world over a LAN connection.


Solution

  • (In the first code you included) The problem is that you add the camera X & Y both when drawing and when processing mouse input. You should change so that when drawing you subtract the camera X & Y instead.

    You can validate the logic by adding a little print statement of the p variable in the logic that creates the ball when clicking with the mouse.

    """This example lets you dynamically create static walls and dynamic balls
    
    """
    __docformat__ = "reStructuredText"
    
    import pygame
    from pygame.locals import *
    from pygame.color import *
    
    import pymunk
    from pymunk import Vec2d
    
    
    X,Y = 0,1
    ### Physics collision types
    COLLTYPE_DEFAULT = 0
    COLLTYPE_MOUSE = 1
    COLLTYPE_BALL = 2
    
    def flipy(y):
        """Small hack to convert chipmunk physics to pygame coordinates"""
        return -y+600
    
    def mouse_coll_func(arbiter, space, data):
        """Simple callback that increases the radius of circles touching the mouse"""
        s1,s2 = arbiter.shapes
        s2.unsafe_set_radius(s2.radius + 0.15)
        return False
    
    def main():            
        pygame.init()
        screen = pygame.display.set_mode((600, 600))
        clock = pygame.time.Clock()
        running = True
    
        # Camera offsets (Nav added)
        cameraX = 0
        cameraY = 0
    
        ### Physics stuff
        space = pymunk.Space()
        space.gravity = 0.0, -900.0
    
        ## Balls
        balls = []
    
        ### Mouse
        mouse_body = pymunk.Body(body_type=pymunk.Body.KINEMATIC)
        mouse_shape = pymunk.Circle(mouse_body, 3, (0,0))
        mouse_shape.collision_type = COLLTYPE_MOUSE
        space.add(mouse_shape)
    
        space.add_collision_handler(COLLTYPE_MOUSE, COLLTYPE_BALL).pre_solve=mouse_coll_func   
    
        ### Static line
        line_point1 = None
        static_lines = []
        run_physics = True
    
        while running:
            for event in pygame.event.get():
                if event.type == QUIT:
                    running = False
                elif event.type == KEYDOWN and event.key == K_ESCAPE:
                    running = False
                elif event.type == KEYDOWN and event.key == K_p:
                    pygame.image.save(screen, "balls_and_lines.png")
                elif event.type == MOUSEBUTTONDOWN and event.button == 1:
                    p = event.pos[X]-cameraX, flipy(event.pos[Y]-cameraY)#Nav added
                    #print("mouseX:"+str(p[0])+" mouseY:"+str(p[1]))
                    body = pymunk.Body(10, 100)
                    body.position = p
                    shape = pymunk.Circle(body, 10, (0,0))
                    shape.friction = 0.5
                    shape.collision_type = COLLTYPE_BALL
                    space.add(body, shape)
                    balls.append(shape)
                #Nav added key detection event
                if event.type == KEYDOWN:
                    if event.key == K_UP:
                        cameraY -= 10
                    if event.key == K_LEFT: 
                        cameraX -= 10
                    if event.key == K_DOWN:
                        cameraY += 10
                    if event.key == K_RIGHT:
                        cameraX += 10                
    
                elif event.type == MOUSEBUTTONDOWN and event.button == 3: 
                    if line_point1 is None:
                        line_point1 = Vec2d(event.pos[X]-cameraX, flipy(event.pos[Y]-cameraY))#Nav added
                elif event.type == MOUSEBUTTONUP and event.button == 3: 
                    if line_point1 is not None:                    
                        line_point2 = Vec2d(event.pos[X]-cameraX, flipy(event.pos[Y]-cameraY))#Nav added
                        body = pymunk.Body(body_type=pymunk.Body.STATIC)
                        shape= pymunk.Segment(body, line_point1, line_point2, 0.0)
                        shape.friction = 0.99
                        space.add(shape)
                        static_lines.append(shape)
                        line_point1 = None
    
                elif event.type == KEYDOWN and event.key == K_SPACE:    
                    run_physics = not run_physics
    
            p = pygame.mouse.get_pos()
            mouse_pos = Vec2d(p[X]-cameraX, flipy(p[Y]-cameraY))#Nav added
            mouse_body.position = mouse_pos
    
    
            if pygame.key.get_mods() & KMOD_SHIFT and pygame.mouse.get_pressed()[0]:
                body = pymunk.Body(10, 10)
                body.position = mouse_pos
                shape = pymunk.Circle(body, 10, (0,0))
                shape.collision_type = COLLTYPE_BALL
                space.add(body, shape)
                balls.append(shape)
    
            ### Update physics
            if run_physics:
                dt = 1.0/60.0
                for x in range(1):
                    space.step(dt)
    
            ### Draw stuff
            screen.fill(THECOLORS["white"])
    
            # Display some text
            font = pygame.font.Font(None, 16)
            text = """LMB: Create ball
                    LMB + Shift: Create many balls
                    RMB: Drag to create wall, release to finish
                    Space: Pause physics simulation"""
            y = 5
            for line in text.splitlines():
                text = font.render(line, 1,THECOLORS["black"])
                screen.blit(text, (5,y))
                y += 10
    
            for ball in balls:           
                r = ball.radius
                v = ball.body.position
                rot = ball.body.rotation_vector
                p = int(v.x)+cameraX, int(flipy(v.y))+cameraY#Nav added
                p2 = Vec2d(rot.x, -rot.y) * r * 0.9
                pygame.draw.circle(screen, THECOLORS["blue"], p, int(r), 2)
                pygame.draw.line(screen, THECOLORS["red"], p, p+p2)
    
            if line_point1 is not None:
                p1 = line_point1.x+cameraX, flipy(line_point1.y-cameraY)#Nav added
                p2 = mouse_pos.x+cameraX, flipy(mouse_pos.y-cameraY)#Nav added
                pygame.draw.lines(screen, THECOLORS["black"], False, [p1,p2])
    
            for line in static_lines:
                body = line.body
    
                pv1 = body.position + line.a.rotated(body.angle)
                pv2 = body.position + line.b.rotated(body.angle)
                p1 = pv1.x+cameraX, flipy(pv1.y)+cameraY#Nav added
                p2 = pv2.x+cameraX, flipy(pv2.y)+cameraY#Nav added
                pygame.draw.lines(screen, THECOLORS["lightgray"], False, [p1,p2])
    
            ### Flip screen
            pygame.display.flip()
            clock.tick(50)
            pygame.display.set_caption("fps: " + str(clock.get_fps()))
    
    if __name__ == '__main__':
        doprof = 0
        if not doprof: 
            main()
        else:
            import cProfile, pstats
    
            prof = cProfile.run("main()", "profile.prof")
            stats = pstats.Stats("profile.prof")
            stats.strip_dirs()
            stats.sort_stats('cumulative', 'time', 'calls')
            stats.print_stats(30)
    

    Note: I read the other question/answer, maybe this follow up question would be better on your other question, since it seems like the 3rd example there more or less answers it?