I've gotten the Conway Game of Life in Python 3.9 from git-hub (there's also a great video on youtube here, made by the author)
All works fine.
Now I'd like to auto-detect when the evolution gets stuck, that is when the cell and cell group shapes are all static or regular oscillators (see pics attached at the end of this post).
My first idea was to compare the next generation grid backwards to 4 generations, that is: if the next generation grid is equal to ny of the previous four generation then it's safe to assume that the evolution has stopped.
So I first thought to make copies of the last 4 grids in 4x2D indipendent arrays and compare at each passage the next generation with each of them.
What would be the most efficient (in terms of time lag) way to compare two bidimentional arrays with say 400 columns and 200 rows?
The scripts use pygame.
here is a copy of what I am using (my plan is to use the check_if_over function in the grid.py module to return True if the evolution has stopped).
MAIN MODULE:
modules = ["pygame", "numpy", "ctypes"]
import sys
import importlib
import subprocess
def install(package):
# install packages if needed
subprocess.check_call([sys.executable, "-m", "pip", "install", package])
# check for needed pacjakes/modules
for needed_module in modules:
try:
i = importlib.import_module(needed_module)
print(f"{needed_module} successfully imported")
except ImportError as e:
install(needed_module)
i = importlib.import_module(needed_module)
except:
"Something went wrong"
import pygame
import os
import grid
import ctypes
from pygame.locals import *
## ADJUSTABLE PARMAS
reduction_factor = 2 # reduces the width/height of the window vs screen size
fps = 60 # max frames per second
scaler = 5 # scales down, the smaller the scaler the greater the number of cells
offset = 1 # thickness of grid separator, must be < scaler
#### COLORS
black = (0, 0, 0)
white = (255, 255, 255)
blue = (0, 14, 71)
os.environ["SDL_VIDEO_CENTERED"] = '1'
user32 = ctypes.windll.user32
screensize = user32.GetSystemMetrics(0), user32.GetSystemMetrics(1)
width, height = int(screensize[0]/reduction_factor), int(screensize[1]/reduction_factor)
size = (width, height)
pygame.init()
pygame.display.set_caption("Conway Game of Life")
screen = pygame.display.set_mode(size)
clock = pygame.time.Clock()
Grid = grid.Grid(width, height, scaler, offset)
Grid.random2d_array()
run = True
# game loop
while run:
clock.tick(fps)
screen.fill(black)
for event in pygame.event.get():
if event.type == QUIT:
run = False
Grid.Conway(off_color=white, on_color=blue, surface=screen)
pygame.display.update()
pygame.quit()
exit(0)
grid.py:
import pygame
import numpy as np
import random
class Grid:
def __init__(self, width, height, scale, offset):
self.scale = scale
self.columns = int(height / scale)
self.rows = int(width / scale)
self.size = (self.rows, self.columns)
self.grid_array = np.ndarray(shape=(self.size))
# next 3 lines defines the set of array copies to save
# the past 4 generations
self.grid_array_copies = []
for i in range(0, 4):
self.grid_array_copies.append(np.ndarray(shape=(self.size)))
self.offset = offset
self.alive = 0
self.evolution_stopped = False
def random2d_array(self):
for x in range(self.rows):
for y in range(self.columns):
self.grid_array[x][y] = random.randint(0, 1)
def Conway(self, off_color, on_color, surface):
for x in range(self.rows):
for y in range(self.columns):
y_pos = y * self.scale
x_pos = x * self.scale
if self.grid_array[x][y] == 1:
pygame.draw.rect(surface, on_color, [x_pos, y_pos, self.scale - self.offset, self.scale - self.offset])
else:
pygame.draw.rect(surface, off_color, [x_pos, y_pos, self.scale - self.offset, self.scale - self.offset])
next = np.ndarray(shape=(self.size))
self.alive = 0
for x in range(self.rows):
for y in range(self.columns):
state = self.grid_array[x][y]
neighbours = self.get_neighbours(x, y)
if state == 0 and neighbours == 3:
next[x][y] = 1
self.alive += 1
elif state == 1 and (neighbours < 2 or neighbours > 3):
next[x][y] = 0
else:
next[x][y] = state
self.alive += state
self.grid_array = next
self.check_if_over(next)
with open("survivors.txt", "w") as f:
f.write(str(self.alive))
def get_neighbours(self, x, y):
total = 0
for n in range(-1, 2):
for m in range(-1, 2):
x_edge = (x + n + self.rows) % self.rows
y_edge = (y + m + self.columns) % self.columns
total += self.grid_array[x_edge][y_edge]
total -= self.grid_array[x][y]
return total
def check_if_over(self, next):
pass
Thanx for the patience.
Initial conditions:
Evolution stopped:
EDIT
I forgot to mention something that may be not that straightforward.
Unlike Golly (another open source for the Conway's Game of Life), the environment where this Game of Life plays is a finite one (see pictures above), that is it's kinda rendering of a spherical suface into a rectangle, so the cells colonies evolving past the right edge of the window re-enter at the left edge, those at the bottom edge re-enter at the top. While in Golly, for example the plan environment is theoretically infinite.
Golly starting conditions, zoomed in:
Golly starting conditions partially zoomed out (could go futher out until cells invisibility and further). The black surface is the environment, the white squares the cells
Related
I have created a simple piano tiles game clone in pygame.
Everything working fine except the way i am generating tiles after every certain interval, but as the game speed increases this leaves a gap between two tiles.
In the original version of the game, there's no lag (0 distance ) between two incoming tiles.
Here's a preview of the game:
Currently I am generating tiles like this:
ADDBLOCK = pygame.USEREVENT + 1
ADDTIME = 650
pygame.time.set_timer(ADDBLOCK, ADDTIME)
if event.type == ADDBLOCK:
x_col = random.randint(0,3)
block = Block(win, (67.5 * x_col, -120))
block_group.add(block)
But with time, these tiles speed increase so there's remain a gap between generation of two tiles as shown by red line in the preview. Is there any way to generate tiles consecutively?
Source Code
Use a variable number to know how many tiles have been generated since the start. This variable will start with 0, then you will add 1 to this variable every time a tile is generated.
Then, you can use a variable like scrolling which increases continuously. You will add this scrolling to every tile y pos to render them.
Now you just have to add a tile which y position is like -tile_height - tile_height * number.
If that doesn't make sense to you, look at this MRE:
import pygame
from pygame.locals import *
from random import randint
pygame.init()
screen = pygame.display.set_mode((240, 480))
clock = pygame.time.Clock()
number_of_tiles = 0
tile_height = 150
tile_surf = pygame.Surface((60, tile_height))
tiles = [] # [column, y pos]
scrolling = 0
score = 0
speed = lambda: 200 + 5*score # increase with the score
time_passed = 0
while True:
click = None # used to click on a tile
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
exit()
if event.type == MOUSEBUTTONDOWN:
click = event.pos
screen.fill((150, 200, 255))
if scrolling > number_of_tiles * tile_height:
# new tile
# use "while" instead of "if" if you want to go at really high speed
tiles.append([randint(0, 3), -tile_height - number_of_tiles * tile_height])
number_of_tiles += 1
for x, y in tiles:
screen.blit(tile_surf, (60 * x, y + scrolling))
if y + scrolling > 480: # delete any tile that is no longer visible
tiles.remove([x, y])
if click is not None and Rect((60 * x, y + scrolling), tile_surf.get_size())
.collidepoint(click):
tiles.remove([x, y]) # delete any tile that has been clicked
score += 1 # used to calculate speed
scrolling += speed() * time_passed
pygame.display.flip()
time_passed = clock.tick() / 1000
As the title says. Using batch drawing I get really good performance, even with 4096 sprites. However, since my sprites need to change their underlying image I run into issues with performance. I'm pretty sure I'm doing something silly here, since I specifically created a grid/sprite sheet to handle this effectively. But, of course, I never really use it in any effective manner. I might as well have had 5 different images.
What I really want is to keep the underlying sprite image constant, but shift the visible part based on the "food" metric. Here's the code:
import sys, pyglet, random, time
# Constants.
WIDTH = 1280
HEIGHT = 960
TARGET_FPS = 60
GROWTH_CHANCE = 0.1
fps = 0
screen = pyglet.window.Window(WIDTH, HEIGHT)
random.seed(time.time())
# Here we load universal assets, images, sounds, etc.
grass_tiles_img = pyglet.image.load('grass_tiles.png')
grass_tiles_grid = pyglet.image.ImageGrid(grass_tiles_img, 1, 5)
# Sprite batches.
grass_batch = pyglet.graphics.Batch()
class GrassTile:
'''Define a grass tile which cows can graze on.'''
def __init__(self, x, y, food):
self.food = food
self.sprite = pyglet.sprite.Sprite(grass_tiles_grid[0], x, y,
batch=grass_batch)
def draw(self):
grid_index = (self.food // 20)
self.sprite.image = grass_tiles_grid[grid_index]
return self.sprite
def grow(self):
if random.random() < GROWTH_CHANCE:
self.food = min(self.food + 1, 99)
#screen.event
def on_close():
sys.exit()
#screen.event
def on_draw():
# Clear the screen.
screen.clear()
# Draw grass.
grass_sprites = []
for grass in grass_tiles:
grass_sprites.append(grass.draw())
grass_batch.draw()
# Draw FPS counter.
label = pyglet.text.Label('FPS: ' + str(fps), 'Times New Roman', 12, 10, 10)
label.draw()
def grow_grass(dt):
for grass in grass_tiles:
grass.grow()
def calculate_fps(dt):
global fps
fps = round(min(pyglet.clock.get_fps(), TARGET_FPS))
grass_tiles = [GrassTile(20 * i, 15 * j, 0) for j in range(64) for i in range(64)]
pyglet.clock.schedule_interval(grow_grass, 1 / TARGET_FPS)
pyglet.clock.schedule_interval(calculate_fps, 1 / TARGET_FPS)
pyglet.app.run()
And here's the image so you can run the code:
https://i.imgur.com/kFe91aA.png
Why not just have the image change during grass.grow()?
You don't need to do anything to the grass in the draw phase except draw the batch. Setting the image of a sprite isn't a draw operation, it just changes texture coordinates.
def grow(self):
if random.random() < GROWTH_CHANCE:
self.food = min(self.food + 1, 99)
grid_index = (self.food // 20)
self.sprite.image = grass_tiles_grid[grid_index]
You also shouldn't be recreating the label every draw frame. Create the label beforehand and just update the text. label.text = f'FPS: {fps}'
Objects all go off in the same line (45 degrees to the left)...
When I extract the random_direction function to test it by itself, it gives the same vectors just flipped 180 or the x is the same and y is the same but negative... stuff like that.
import pygame
import os
import math
import random
from pygame.math import Vector2
def random_direction():
vector = Vector2(random.uniform(-max_speed, max_speed), random.uniform(-max_speed, max_speed))
if vector.length() == 0:
return vector
else:
return Vector2.normalize(vector)
def scaled(vector, scale):
if vector.length() == 0:
return vector
else:
return Vector2.normalize(vector) * scale
def clamped(vector, limit):
if vector.length() <= limit or vector.length() == 0:
return vector
else:
return Vector2.normalize(vector) * limit
def shoot():
for i in range(len(boids)):
boids[i]['velocity'] = boids[i]['desired_direction'] * max_speed
boids[i]['boid'].x += boids[i]['velocity'].x
boids[i]['boid'].y += boids[i]['velocity'].x
# if boids[i]['boid'].x >= WIDTH:
# boids[i]['boid'].x = 0
# elif boids[i]['boid'].x <= 0:
# boids[i]['boid'].x = WIDTH
# elif boids[i]['boid'].y >= HEIGHT:
# boids[i]['boid'].y = 0
# elif boids[i]['boid'].y <= 0:
# boids[i]['boid'].y = HEIGHT
def draw_window():
WIN.fill((0, 0, 0))
# for i in range(n):
# rot_image = pygame.transform.rotate(image_scaled, math.degrees(math.atan2(boids[i]['velocity'].x, boids[i]['velocity'].y)) +180)
# WIN.blit(rot_image, (boids[i]['boid'].x - int(rot_image.get_width()/2), boids[i]['boid'].y - int(rot_image.get_height()/2))) #########
for i in range(len(boids)):
WIN.blit(image_scaled, (boids[i]['boid'].x, boids[i]['boid'].y))
pygame.display.update()
WIDTH, HEIGHT = 1440, 720 #1680, 990
WIN = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption('Sim')
FPS = 60
image = pygame.image.load(os.path.join('Assets', 'long_fish.png'))
image_width, image_height = 40, 40
image_scaled = pygame.transform.scale(image, (image_width, image_height))
#boid = pygame.Rect(WIDTH/2, HEIGHT/2, image_width, image_height)
max_speed = 10 #2
steer_strength = 0.04 #2
wander_strength = 0.4 #0.2
# desired_direction = Vector2(0, 0)
# velocity = Vector2(0, 0)
# shoot_direction = random_direction()
n = 30
boids = []
for i in range(n):
boids.append({'boid': pygame.Rect(WIDTH/2, HEIGHT/2, image_width, image_height),
'desired_direction': random_direction(),
'velocity': Vector2(0,0)})
def main():
clock = pygame.time.Clock()
run = True
while run:
clock.tick(FPS)
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
shoot()
draw_window()
pygame.quit()
if __name__ == '__main__':
main()
I've made things like this before on pygame and they work but I'm not sure why they do and this doesn't.
Your vector generator generates with bounds of a square:
because of the way you set it up. You then normalize the vector, putting whatever it raised into the bounding circle. Because of this method, values nearer to the corners are more likely to be chosen. From the origin to the edge, there is a distance of one. From the origin to the corner, there is a distance of 1.41 . Thus, values that normalize to a corner are more likely to be chosen
This can give you the impression of not having truly random values, as some values pop up more frequently than others.
The way around this is to generate an already normalized vector, py choosing a point from a circle.
The best way to do this is to generate an angle, in radians. Ex:
>>> angle = math.radians(random.randint(0, 360))
Then, use some basic trigonometry to turn that angle into a point
>>> x = math.cos(angle)
>>> y = math.sin(angle)
The tuple (x, y) should be an unbiased value, that is as random as your pseudorandom (how computers do random, it's actually a big complex equation that generates a value really close to random, but it actually isn't) generator will get.
Implementing this into your code:
def random_direction():
a = math.radians(random.randint(0, 360))
return pygame.Vector2(math.cos(a), math.sin(a))
I've decided to create a simpler version of the Game of Life since I couldn't really work with my old one. I now have a GameOfLife class, but it is running very slow. When I click on a cell, it takes some time before it activates/deactivates, etc. Does anyone know why it's slow? Is there something wrong with the FPSCLOCK, or am I running something that shouldn't be running?
import pygame
# Defining the grid dimensions.
GRID_SIZE = width, height = 500, 500
# Defining the size of the cells, and how many cells there are in the x and y direction.
CELL_SIZE = 10
X_CELLS = int(width/CELL_SIZE)
Y_CELLS = int(height/CELL_SIZE)
# Defining a color for dead cells (background) and alive cells.
COLOR_DEAD = 0 #background
COLOR_ALIVE = 1 #alive_cell
colors = []
colors.append(( 0, 0, 0)) #Black
colors.append((0, 128, 128)) #blue
# Two lists, one for the current generation, and one for the next generation, so you can have iterations.
current_generation = [[COLOR_DEAD for y in range(Y_CELLS)] for x in range(X_CELLS)]
next_generation = [[COLOR_DEAD for y in range(Y_CELLS)] for x in range(X_CELLS)]
fps_max = 10
class GameOfLife:
def __init__(self):
self.next_iteration = False
self.game_over = False
# Starting pygame
pygame.init()
pygame.display.set_caption("Game of Life - Created by") # Gives a title to the window
self.screen = pygame.display.set_mode(GRID_SIZE) # Create the window with the GRID_SIZE.
#Clock to set the FPS
self.FPSCLOCK = pygame.time.Clock()
# Initialise the generations
self.init_gen(current_generation, COLOR_DEAD)
# Initializing all the cells.
def init_gen(self, generation, c):
for y in range(Y_CELLS):
for x in range(X_CELLS):
generation[x][y] = c
# Drawing the cells, color black or blue at location x/y.
def draw_cell(self, x, y, c):
pos = (int(x * CELL_SIZE + CELL_SIZE / 2),
int(y * CELL_SIZE + CELL_SIZE / 2))
# pygame.draw.rect(screen, colors[c], pygame.Rect(x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE-1, CELL_SIZE-1))
# pygame.draw.circle(screen, colors[c], pos, CELL_SIZE, CELL_SIZE) #Weird form, can also be used instead of rectangles
pygame.draw.circle(self.screen, colors[c], pos, 5,0) # Use the last two arguments (radius, width) to change the look of the circles.
pygame.display.flip()
# Updating the cells.
def update_gen(self):
global current_generation
for y in range(Y_CELLS):
for x in range(X_CELLS):
c = next_generation[x][y]
self.draw_cell(x, y, c)
# Update current_generation
current_generation = list(next_generation)
# Activate a living cell
def activate_living_cell(self, x, y):
global next_generation
next_generation[x][y] = COLOR_ALIVE
# Deactivate a living cell
def deactivate_living_cell(self, x, y):
global next_generation
next_generation[x][y] = COLOR_DEAD
# Function to check neighbor cell
def check_cells(self, x, y):
# Ignoring cells off the edge
if (x < 0) or (y < 0): return 0
if (x >= X_CELLS) or (y >= Y_CELLS): return 0
if current_generation[x][y] == COLOR_ALIVE:
return 1
else:
return 0
def check_cell_neighbors(self, row_index, col_index):
# Get the number of alive cells surrounding the current cell
num_alive_neighbors = 0
num_alive_neighbors += self.check_cells(row_index - 1, col_index - 1)
num_alive_neighbors += self.check_cells(row_index - 1, col_index)
num_alive_neighbors += self.check_cells(row_index - 1, col_index + 1)
num_alive_neighbors += self.check_cells(row_index, col_index - 1)
num_alive_neighbors += self.check_cells(row_index, col_index + 1)
num_alive_neighbors += self.check_cells(row_index + 1, col_index - 1)
num_alive_neighbors += self.check_cells(row_index + 1, col_index)
num_alive_neighbors += self.check_cells(row_index + 1, col_index + 1)
return num_alive_neighbors
# Rules
# 1 Any live cell with fewer than two live neighbors dies, as if by underpopulation.
# 2 Any live cell with two or three live neighbors lives on to the next generation.
# 3 Any live cell with more than three live neighbors dies, as if by overpopulation.
# 4 Any dead cell with exactly three live neighbors becomes a live cell, as if by reproduction.
def create_next_gen(self):
for y in range(Y_CELLS):
for x in range(X_CELLS):
# If cell is live, count neighboring live cells
n = self.check_cell_neighbors(x, y) # number of neighbors
c = current_generation[x][y] # current cell (either dead or alive).
if c == COLOR_ALIVE: # If the cell is living:
if (n < 2): # Rule number 1, underpopulation
next_generation[x][y] = COLOR_DEAD
elif (n > 3): # Rule number 3, overpopulation
next_generation[x][y] = COLOR_DEAD
else: # Rule number 3, 2 or 3 neighbors, staying alive.
next_generation[x][y] = COLOR_ALIVE
else: # if the cell is dead:
if (n == 3):
# Rule number 4: A dead cell with three living neighbors becomes alive.
next_generation[x][y] = COLOR_ALIVE
# Runs the game loop
def handle_events(self):
for event in pygame.event.get():
if event.type == pygame.QUIT:
self.game_over = True # If pressing the quit button, it closes the window.
if event.type == pygame.MOUSEBUTTONDOWN: # if pressing the mouse button, it gets the position. If the cell is dead, make it alive, if the cell is alive, make it dead.
posn = pygame.mouse.get_pos()
x = int(posn[0] / CELL_SIZE)
y = int(posn[1] / CELL_SIZE)
if next_generation[x][y] == COLOR_DEAD:
self.activate_living_cell(x, y)
else:
self.deactivate_living_cell(x, y)
# Check for q, g, s or w keys
if event.type == pygame.KEYDOWN: # keydown --> quits when the button goes down. keyup --> quits when the button goes up again.
if event.unicode == 'q': # Press q to quit.
self.game_over = True
print("q")
elif event.key == pygame.K_SPACE: # Space for the next iteration manually.
self.create_next_gen()
print("keypress")
elif event.unicode == 'a': # a to automate the iterations.
self.next_iteration = True
print("a")
elif event.unicode == 's': # s to stop the automated iterations.
self.next_iteration = False
print("s")
elif event.unicode == 'r': # r to reset the grid.
self.next_iteration = False
self.init_gen(next_generation, COLOR_DEAD)
print("r")
def run(self):
while not self.game_over:
# Set the frames per second.
self.handle_events()
if self.next_iteration: # if next iteration is true, the next gen is created according to the rules.
self.create_next_gen()
# Updating
self.update_gen()
self.FPSCLOCK.tick(fps_max)
if __name__ == "__main__":
game = GameOfLife()
game.run()
The pygame.display.flip() call redraws the whole screen - and you are calling it once for each redrawn cell.
Move that call from the draw_cell method above to the end of the method gen_update() and you should be fine. Better yet, just remove it from any methods, and place it at the end of the main loop, just before calling the clock tick:
def run(self):
while not self.game_over:
# Set the frames per second.
self.handle_events()
if self.next_iteration: # if next iteration is true, the next gen is created according to the rules.
self.create_next_gen()
# Updating
self.update_gen()
pygame.display.flip()
self.FPSCLOCK.tick(fps_max)
Why is the physics wrong in the following Pymunk example?
from __future__ import print_function
import sys
from math import pi
import pygame
from pygame.locals import USEREVENT, QUIT, KEYDOWN, KEYUP, K_s, K_r, K_q, K_ESCAPE, K_UP, K_DOWN, K_LEFT, K_RIGHT
from pygame.color import THECOLORS
import pymunk
from pymunk import Vec2d
import pymunk.pygame_util
LEG_GROUP = 1
class Simulator(object):
def __init__(self):
self.display_flags = 0
self.display_size = (600, 600)
self.space = pymunk.Space()
self.space.gravity = (0.0, -1900.0)
self.space.damping = 0.999 # to prevent it from blowing up.
# Pymunk physics coordinates start from the lower right-hand corner of the screen.
self.ground_y = 100
ground = pymunk.Segment(self.space.static_body, (5, self.ground_y), (595, self.ground_y), 1.0)
ground.friction = 1.0
self.space.add(ground)
self.screen = None
self.draw_options = None
def reset_bodies(self):
for body in self.space.bodies:
if not hasattr(body, 'start_position'):
continue
body.position = Vec2d(body.start_position)
body.force = 0, 0
body.torque = 0
body.velocity = 0, 0
body.angular_velocity = 0
body.angle = body.start_angle
def draw(self):
### Clear the screen
self.screen.fill(THECOLORS["white"])
### Draw space
self.space.debug_draw(self.draw_options)
### All done, lets flip the display
pygame.display.flip()
def main(self):
pygame.init()
self.screen = pygame.display.set_mode(self.display_size, self.display_flags)
width, height = self.screen.get_size()
self.draw_options = pymunk.pygame_util.DrawOptions(self.screen)
def to_pygame(p):
"""Small hack to convert pymunk to pygame coordinates"""
return int(p.x), int(-p.y+height)
def from_pygame(p):
return to_pygame(p)
clock = pygame.time.Clock()
running = True
font = pygame.font.Font(None, 16)
# Create the torso box.
box_width = 50
box_height = 100
# leg_length = 100
leg_length = 125
leg_thickness = 2
leg_shape_filter = pymunk.ShapeFilter(group=LEG_GROUP)
# Create torso.
mass = 200
points = [(-box_width/2, -box_height/2), (-box_width/2, box_height/2), (box_width/2, box_height/2), (box_width/2, -box_height/2)]
moment = pymunk.moment_for_poly(mass, points)
body1 = pymunk.Body(mass, moment)
body1.position = (self.display_size[0]/2, self.ground_y+box_height/2+leg_length)
body1.start_position = Vec2d(body1.position)
body1.start_angle = body1.angle
shape1 = pymunk.Poly(body1, points)
shape1.filter = leg_shape_filter
shape1.friction = 0.8
shape1.elasticity = 0.0
self.space.add(body1, shape1)
# Create leg extending from the right to the origin.
mass = 10
points = [
(leg_thickness/2, -leg_length/2),
(-leg_thickness/2, -leg_length/2),
(-leg_thickness/2, leg_length/2),
(leg_thickness/2, leg_length/2)
]
moment = pymunk.moment_for_poly(mass, points)
body2 = pymunk.Body(mass, moment)
body2.position = (self.display_size[0]/2-box_width/2+leg_thickness/2, self.ground_y+leg_length/2)
body2.start_position = Vec2d(body2.position)
body2.start_angle = body2.angle
shape2 = pymunk.Poly(body2, points)
shape2.filter = leg_shape_filter
shape2.friction = 0.8
shape2.elasticity = 0.0
self.space.add(body2, shape2)
# Link bars together at end.
pj = pymunk.PivotJoint(body1, body2, (self.display_size[0]/2-box_width/2, self.ground_y+leg_length))
self.space.add(pj)
# Attach the foot to the ground in a fixed position.
# We raise it above by the thickness of the leg to simulate a ball-foot. Otherwise, the default box foot creates discontinuities.
pj = pymunk.PivotJoint(self.space.static_body, body2, (self.display_size[0]/2-box_width/2, self.ground_y+leg_thickness))
self.space.add(pj)
# Actuate the bars via a motor.
motor_joint = pymunk.SimpleMotor(body1, body2, 0)
motor_joint.max_force = 1e10 # mimicks default infinity
# motor_joint.max_force = 1e9
# motor_joint.max_force = 1e7 # too weak, almost no movement
self.space.add(motor_joint)
# Add hard stops to leg pivot so the leg can't rotate through the torso.
hip_limit_joint = pymunk.RotaryLimitJoint(body1, body2, -pi/4., pi/4.) # -45deg:+45deg
self.space.add(hip_limit_joint)
last_body1_pos = None
last_body1_vel = None
simulate = False
while running:
# print('angles:', body1.angle, body2.angle)
# print('torso force:', body1.force)
print('body1.position: %.02f %.02f' % (body1.position.x, body1.position.y))
current_body1_vel = None
if last_body1_pos:
current_body1_vel = body1.position - last_body1_pos
print('current_body1_vel: %.02f %.02f' % (current_body1_vel.x, current_body1_vel.y))
current_body1_accel = None
if last_body1_vel:
current_body1_accel = current_body1_vel - last_body1_vel
print('current_body1_accel: %.02f %.02f' % (current_body1_accel.x, current_body1_accel.y))
servo_angle = (body1.angle - body2.angle) * 180/pi # 0 degrees means leg is angled straight down
servo_cw_enabled = servo_angle > -45
servo_ccw_enabled = servo_angle < 45
for event in pygame.event.get():
if event.type == QUIT or (event.type == KEYDOWN and event.key in (K_q, K_ESCAPE)):
sys.exit(0)
elif event.type == KEYDOWN and event.key == K_s:
# Start/stop simulation.
simulate = not simulate
last_body1_pos = Vec2d(body1.position)
if current_body1_vel:
last_body1_vel = Vec2d(current_body1_vel)
self.draw()
### Update physics
fps = 50
iterations = 25
dt = 1.0/float(fps)/float(iterations)
if simulate:
for x in range(iterations): # 10 iterations to get a more stable simulation
self.space.step(dt)
pygame.display.flip()
clock.tick(fps)
if __name__ == '__main__':
sim = Simulator()
sim.main()
This renders a box placed on top of a thin leg. The leg is connected to the box by a pivot joint, and to the ground via another pivot joint. However, the leg is attached to the box off-center to the left, so the center-of-gravity is unbalanced. In the real world, this setup would cause the box to toppled to the right. However, when you run this code (and press "s" to start), it shows the box toppling to the left. Why is this?
I've tried adjusting the mass (high mass for the box, low mass for the leg), the center of gravity for the box, and tweaking the attachment points for the joints, but nothing seems to change the outcome. What am I doing wrong?
I want to use this for simulating a real-world phenomena, but until I can get it to reproduce the real-world phenomena, I'm stuck.
It seems to be because the leg shape collides with the bottom ground shape.
The easiest way to make them not collide is to move them apart a little bit. For example make the leg a little shorter so that it doesnt touch the ground.
Another solution is to do as you did in your other question, ignore collisions between the leg and the ground. To do that you can setup a shape filter, but since you probably want to keep the box from colliding with the leg, and at the same time count collisions between the box and ground I think you need to use the categories/masks of the shape filter as documented here: http://www.pymunk.org/en/latest/pymunk.html#pymunk.ShapeFilter