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.
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.
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.
(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?