Gravo2D

Yesterday I wanted to remake my Gravo project using Pygame. It’s Christmas break, so I was actually able to do that.

Here I represent each particle with a line of a length proportional to its speed:

gravo2d-0-force-spokes

The white lines represent each force acting on a particle at any given time. When two particles pass near each other, the forces grow larger and the lines reach farther.

The twinkling effect is rather cool, but I was more interested in the movement of the ends of those force lines. Further experimentation led me to create this:

gravo2d-a

Here, lines are drawn following each particle, but floating away from it at a distance determined by a force. Each particle has a line corresponding to each other particle. these lines create a figure 8 or an hourglass when two particles slingshot around each other. the color of the line is also determined by the forces acting on a particle (bright green represents a strong horizontal force, bright blue represents a strong vertical force)

I found that I could add at least 64 particles without significant lag:

gravo2d-b-more-particles

Finally, I began to experiment with zooming effects. previously, I have been dimming  the screen and painting over it again to produce the echo. Now I store the current frame to be painted in the background of the next frame.

gravo2d-d-smooth-zoom

Here’s my code:

import random as rand
import pygame
import pygame.event
import math

size = [720,720]
screen = pygame.display.set_mode([1280,720])
pygame.display.set_caption("gravo2d")

back = pygame.Surface([1280,720])

class particle:
	
	def __init__(self, groupSize):
		self.pos = (rand.uniform(0.0,1.77777777), rand.uniform(0.0,1.0)) #start anywhere
		self.vel = (rand.uniform(-0.01,0.01), rand.uniform(-0.01,0.01)) #start with a random velocity
		self.charge = rand.uniform(0.5,1.5)*(rand.randint(0,1)-0.5) #charge varies but is never nearly 0
		self.mass = rand.uniform(0.95, 1.05) #mass affects response to forces
		self.color = [int(128+self.charge*128),0,int(128-self.charge*128)] #particle color is based on charge
		self.forces = [] #all forces between this particle and particles of higher index (longest for the first particles)
		self.forceIndex = 0 #the force currently being edited by the simulation
		self.sharedForce = (0.0,0.0) #the equal and opposite force that the simulator uses for the other particle in the pair
		for i in range(groupSize+1):
			self.forces.append((0.0,0.0))
		self.netForce = (0.0,0.0) #modified many times before being acted upon and reset to 0
		
	def addForce(self, drawOnto, f): #used to inform a particle of the force from another particle.
		#add force to net forces
		self.netForce = (self.netForce[0]+f[0],self.netForce[1]+f[1])
		#color with velocity influencing red component, horizontal force for green and verticle force for blue.
		color = [int(511*abs(self.vel[0]+self.vel[1])),(160*math.atan(10000.0*abs(f[0]))),(160*math.atan(10000.0*abs(f[1])))]
		#represent increase in force with a line who's distance from the particle increases.
		self.drawLine(drawOnto, color, [self.pos[0]-self.vel[0]+self.forces[self.forceIndex][0]*64,self.pos[1]-self.vel[1]+self.forces[self.forceIndex][1]*64], [self.pos[0]+f[0]*64,self.pos[1]+f[1]*64], 4)
		#forces[forceIndex] stores this force until next tick  to start the next line segment at the end of this one. sharedForce holds the last force when recording this force, so that the subseuent simulation of the other particle in this pair can use it later in the tick.
		self.sharedForce = self.forces[self.forceIndex]
		self.forces[self.forceIndex] = (f[0],f[1])
		#next time, dealing with a force from a different particle, remember a different last force.
		self.forceIndex += 1
		return color
		
	def acceptForce(self, drawOnto, f, previous, color): #used when an unknown particle of lower index forces this one.
		self.netForce = (self.netForce[0]-f[0],self.netForce[1]-f[1])
		self.drawLine(drawOnto, color, [self.pos[0]-self.vel[0]-previous[0]*64,self.pos[1]-self.vel[1]-previous[1]*64], [self.pos[0]-f[0]*64,self.pos[1]-f[1]*64], 4)
		
	def move(self): #move based on velocity
		self.pos = (self.pos[0]+self.vel[0],self.pos[1]+self.vel[1])
		
	def react(self): #accelerate based on net force
		self.vel = (self.vel[0]+self.netForce[0]*0.1*self.mass,self.vel[1]+self.netForce[1]*0.1*self.mass)
		self.netForce = (0.0,0.0)
		self.forceIndex = 0
		
	def bounce(self): #bounce off the walls
		if (abs(self.pos[0]-0.88888888)>0.88888889): #left and right sides
			self.vel = (self.vel[0]*-0.9,self.vel[1]*0.9) #reverse direction
			#find new position by mirroring over the edge once past it:
			self.pos = (abs(self.pos[0]) if self.pos[0]0.5): #top and bottom sides
			self.vel = (self.vel[0]*0.9,self.vel[1]*-0.9)
			self.pos = (self.pos[0], abs(self.pos[1]) if self.pos[1]

Leave a comment