# Filename: A16b_simple_airtrack_forces.py # Written by: James D. Miller """ Air Track with point forces... Self-contained pygame-based example of pybox2d. This file is based on the simple_01.py example in the pybox2d example directory. """ import pygame from pygame.locals import * # import Box2D # The main library from Box2D import * # This maps Box2D.b2Vec2 to vec2, and so on. (confusing, so disabled it) #from Box2D.b2 import * #===================================================================== # Classes #===================================================================== class fwQueryCallback(b2QueryCallback): # Checks for objects at particular locations (p) like under the cursor. def __init__(self, p): super(fwQueryCallback, self).__init__() self.point = p self.fixture = None def ReportFixture(self, fixture): body = fixture.body if body.type == b2_dynamicBody: inside=fixture.TestPoint(self.point) if inside: self.fixture=fixture # We found the object, so stop the query return False # Continue the query return True class Environment: def __init__(self, screensize_tuple, world): self.world = world self.viewZoom = 10.0 self.viewCenter = b2Vec2(0, 0.0) self.viewOffset = b2Vec2(0, 0) self.screenSize = b2Vec2(*screensize_tuple) self.rMouseDown = False # self.textLine = 30 # self.font = None # self.fps = 0 self.mouseJoint = None # Needed for the mousejoint self.groundbody = self.world.CreateBody() self.flipX = False self.flipY = True # pass viewZoom to init in DrawToScreen class. Call the DrawToScreen function by use of the "renderer" name. self.renderer = DrawToScreen(self.viewZoom) self.pointSize = 2.5 self.colors={ 'mouse_point' : b2Color(1,0,0), 'bomb_center' : b2Color(0,0,1.0), 'joint_line' : b2Color(0.8,0.8,0.8), 'contact_add' : b2Color(0.3, 0.95, 0.3), 'contact_persist' : b2Color(0.3, 0.3, 0.95), 'contact_normal' : b2Color(0.4, 0.9, 0.4), 'force_point' : b2Color(0,1,0) } def MouseDown(self, p): """ Indicates that there was a left click at point p (world coordinates) """ # If there is already a mouse joint just get out of here. if self.mouseJoint != None: return # Create a mouse joint on the selected body (assuming it's dynamic) # Make a small box. aabb = b2AABB(lowerBound=p-(0.001, 0.001), upperBound=p+(0.001, 0.001)) # Query the world for overlapping shapes. query = fwQueryCallback(p) self.world.QueryAABB(query, aabb) if query.fixture: body = query.fixture.body # A body was selected, create the mouse joint self.mouseJoint = self.world.CreateMouseJoint( bodyA=self.groundbody, bodyB=body, target=p, maxForce=1000.0*body.mass) body.awake = True def MouseUp(self, p): """ Left mouse button up. """ if self.mouseJoint: self.world.DestroyJoint(self.mouseJoint) self.mouseJoint = None def MouseMove(self, p): """ Mouse moved to point p, in world coordinates. """ self.mouseWorld = p if self.mouseJoint: self.mouseJoint.target = p def CheckKeys(self): """ Check the keys that are evaluated on every main loop iteration. I.e., they aren't just evaluated when first pressed down """ pygame.event.pump() self.keys = keys = pygame.key.get_pressed() if keys[self.Keys.K_LEFT]: self.viewCenter -= (0.5, 0) elif keys[self.Keys.K_RIGHT]: self.viewCenter += (0.5, 0) if keys[self.Keys.K_UP]: self.viewCenter += (0, 0.5) elif keys[self.Keys.K_DOWN]: self.viewCenter -= (0, 0.5) if keys[self.Keys.K_HOME]: self.viewZoom = 1.0 self.viewCenter = (0.0, 20.0) def Keyboard_Event(self, key, down=True): global apply_jet_1_TF, apply_jet_2_TF if down: if key==K_z: # Zoom in self.viewZoom = min(1.1 * self.viewZoom, 50.0) elif key==K_x: # Zoom out self.viewZoom = max(0.9 * self.viewZoom, 0.02) elif key == K_f: # Toggle the force. apply_jet_1_TF = not apply_jet_1_TF #print "applying force 1" elif key == K_g: # Toggle the force. apply_jet_2_TF = not apply_jet_2_TF #print "applying force 2" else: pass # If up else: if key == K_f: # Toggle the force. apply_jet_1_TF = not apply_jet_1_TF elif key == K_g: # Toggle the force. apply_jet_2_TF = not apply_jet_2_TF else: pass def checkEvents(self): """ Check for pygame events (mainly keyboard/mouse events). Passes the events onto the GUI also. """ # print "checkEvents" for event in pygame.event.get(): # print "event.type = ", event.type if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE): print "early bailout" return False elif event.type == KEYDOWN: self.Keyboard_Event(event.key, down=True) elif event.type == KEYUP: self.Keyboard_Event(event.key, down=False) elif event.type == MOUSEBUTTONDOWN: #print "in MouseButtonDown block" p = self.ConvertScreenToWorld(*event.pos) if event.button == 1: # left self.MouseDown( p ) elif event.button == 2: #middle pass elif event.button == 3: #right self.rMouseDown = True elif event.button == 4: self.viewZoom *= 1.1 #print "self.viewZoom", self.viewZoom elif event.button == 5: self.viewZoom /= 1.1 elif event.type == MOUSEBUTTONUP: p = self.ConvertScreenToWorld(*event.pos) if event.button == 3: #right self.rMouseDown = False else: self.MouseUp(p) elif event.type == MOUSEMOTION: p = self.ConvertScreenToWorld(*event.pos) self.MouseMove(p) if self.rMouseDown: print "in mousemotion block", event.pos, event.rel[0], event.rel[1] self.viewCenter -= (event.rel[0]/1.0, -event.rel[1]/1.0) #... it was /5.0 return True def ConvertScreenToWorld(self, x, y): # self.viewOffset = self.viewCenter - self.screenSize/2 self.viewOffset = self.viewCenter return b2Vec2((x + self.viewOffset.x) / self.viewZoom, ((self.screenSize.y - y + self.viewOffset.y) / self.viewZoom)) def ConvertWorldtoScreen(self, point): """ Convert from world to screen coordinates. In the class instance, we store a zoom factor, an offset indicating where the view extents start at, and the screen size (in pixels). """ # The zoom factor works to define and scale the relationship between pixels (screen) and meters (world). self.viewOffset = self.viewCenter x = (point.x * self.viewZoom) - self.viewOffset.x if self.flipX: x = self.screenSize.x - x y = (point.y * self.viewZoom) - self.viewOffset.y if self.flipY: y = self.screenSize.y - y return (int(round(x)), int(round(y))) # return tuple of integers def drawMouseJoint(self): if self.mouseJoint: p1_screen = self.ConvertWorldtoScreen(self.mouseJoint.anchorB) #The point on the object converted to screen coordinates. p2_screen = self.ConvertWorldtoScreen(self.mouseJoint.target) #The current mouse position converted to screen coordinates. self.renderer.DrawPoint(p1_screen, self.pointSize, self.colors['mouse_point']) self.renderer.DrawPoint(p2_screen, self.pointSize, self.colors['mouse_point']) self.renderer.DrawSegment(p1_screen, p2_screen, self.colors['joint_line']) def drawForcePoint(self, forcePoint_2d_m): forcePoint_screen = self.ConvertWorldtoScreen( forcePoint_2d_m) self.renderer.DrawPoint( forcePoint_screen, self.pointSize, self.colors['force_point']) class DrawToScreen: def __init__(self, viewZoom): self.viewZoom = viewZoom self.surface = screen def DrawPoint(self, p, size, color): """ Draw a single point at point p given a pixel size and color. """ self.DrawCircle(p, size/self.viewZoom, color, drawwidth=0) def DrawSegment(self, p1, p2, color): """ Draw the line segment from p1-p2 with the specified color. """ pygame.draw.aaline(self.surface, color.bytes, p1, p2) def DrawCircle(self, center, radius, color, drawwidth=1): """ Draw a wireframe circle given the center, radius, axis of orientation and color. """ radius *= self.viewZoom if radius < 1: radius = 1 else: radius = int(radius) pygame.draw.circle(self.surface, color.bytes, center, radius, drawwidth) #================================================================================= # Some functions #================================================================================= def AddCar(theWorld, bodies, x=0, vx=0, width=1, height=1, my_restitution=1.00): # Create a dynamic body dynamic_body = theWorld.CreateDynamicBody(position=b2Vec2(x, 0), angle=0, linearVelocity=b2Vec2(vx, 0)) # Add to the bodies list bodies.append( dynamic_body) # And add a box fixture onto it (with a nonzero density, so it will move) dynamic_body.CreatePolygonFixture(box=(width, height), density=1.0, friction=0.00, restitution=my_restitution) # Note the area used in the mass calc will be area = (2 * w) * (2 * h) # Then mass = density * area print "Mass data:", dynamic_body.mass #================================================================================= # Main program #================================================================================= # --- constants --- TARGET_FPS = 120 TIME_STEP = 1.0/TARGET_FPS screenXY = SCREEN_WIDTH, SCREEN_HEIGHT = 1140, 480 # --- pygame setup --- pygame.init() screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT), 0, 32) pygame.display.set_caption('Simple pygame example') clock = pygame.time.Clock() # --- pybox2d world setup --- # Create the world world = b2World(gravity=(-0.0, -10.0), doSleep=True) # Initialize the environment object (kind of like my own framework). e = Environment(screenXY, world) #Keys = TheKeys() # List of bodies. bodies = [] # And a static body to be the base of the air track. ground_body = world.CreateStaticBody( position=(0,0), shapes=b2PolygonShape(box=(150,0.5)) ) bodies.append( ground_body) # left bumper air_track_bumper1 = world.CreateStaticBody( position=(0,0), shapes=b2PolygonShape(box=(0.5,5)) ) bodies.append( air_track_bumper1) # right bumper air_track_bumper2 = world.CreateStaticBody( position=(100,0), shapes=b2PolygonShape(box=(0.5,5)) ) bodies.append( air_track_bumper2) # Add cars to the track x = 15 for j in range(5): x += 5 vx = j + 2 #vx = -6 AddCar(world, bodies, x, vx, height=1.0, width=2.0, my_restitution=0.90) colors = { b2_staticBody : (255,255,255,255), b2_dynamicBody : (127,127,127,255), } # Initialize the jet toggles. apply_jet_1_TF = False apply_jet_2_TF = False # --- main game loop --- running = True while running: # Use the Environment instance "e" to check events running = e.checkEvents() # e.CheckKeys() # Apply forces if apply_jet_1_TF: force_point_1_2d_m = bodies[5].GetWorldPoint(b2Vec2(2,0)) bodies[5].ApplyForce(force=b2Vec2(0.0, 100.0), point=force_point_1_2d_m, wake=True) if apply_jet_2_TF: force_point_2_2d_m = bodies[5].GetWorldPoint(b2Vec2(-2,0)) bodies[5].ApplyForce(force=b2Vec2(0.0, 100.0), point=force_point_2_2d_m, wake=True) # Make Box2D simulate the physics of our world for one step. # Instruct the world to perform a single step of simulation. It is # generally best to keep the time step and iterations fixed. # See the manual (Section "Simulating the World") for further discussion # on these parameters and their implications. world.Step(TIME_STEP, 10, 10) screen.fill((0,0,0,0)) # Draw the world for body in bodies: # or: world.bodies # The body gives us the position and angle of its shapes for fixture in body.fixtures: # The fixture holds information like density and friction, # and also the shape. shape = fixture.shape # This assumes that this is a polygon shape. (not good normally!) # We take the body's transform and multiply it with each # vertex, and then convert from world to screen. # The "*" operators are overloaded from the class (operator overloading). vertices_screen = [] for vertex_object in shape.vertices: vertex_world = body.transform * vertex_object # Overload operation vertex_screen = e.ConvertWorldtoScreen( vertex_world) # This returns a tuple vertices_screen.append( vertex_screen) # Append to the list. pygame.draw.polygon(screen, colors[body.type], vertices_screen) # Draw mouse joint. e.drawMouseJoint() # Draw force points. if apply_jet_1_TF: e.drawForcePoint( force_point_1_2d_m) if apply_jet_2_TF: e.drawForcePoint( force_point_2_2d_m) # Flip the screen and try to keep at the target FPS pygame.display.flip() clock.tick(TARGET_FPS) pygame.quit() print('Done!')