关于此项目——Chaos Emulator
我是Osmos游戏的狂热分子,因为对Osmos中的星体运动,尤其是吸引体的引力模拟极为感兴趣,所以编写了Chaos Emulator这个简单的引力模拟器。引力模拟器的概念大部分与Osmos相同,所以建议先了解Osmos这个伟大的物理游戏。
引力模拟器是开源的,遵循简单粗暴的MIT协议。GitHub仓库在这里
(请原谅代码格式问题,因为此程序目前仅个人开发)
给这个项目起名字为Chaos(混沌)的原因是,前不久才真正认识了混沌理论(或者是一种思想观)。而另一个原因是,我被这个词的发音吸引了……原来它读作/'keɪɒs/。
0.2.3的新功能
此版本目前仍旧保留一颗吸引体和一颗伴星的运作模式。默认地,吸引体会吸收伴星,而无论它们谁的质量更大。
在0.2.3版本中,添加了新的渲染方案,并且可使用如下的键盘操作:
- S键:让吸引体变得和普通星体大小相等
- A键:让普通星体缩小
- D键:让普通星体变大
- ←键:让吸引体缩小
- →键:让吸引体变大
项目效果和原理
以下是程序截图:
通过指定全局常量FILL
的布尔值来指示是否在每帧渲染前擦除上一帧的图像。
图中所示的骤然缩小的残迹,是吸引体吸收伴星时产生的。吸收过程的模拟添加于v0.2.2,但是现在尚未进行精确优化,所以在吸引体小于伴星时吸收的模拟会偏差。
项目使用离散化的引力计算方法,即根据星体当前的位置和惯性等属性来计算受引力影响的下一帧的位置,而并未采用数学轨迹模型的静态预计算方式。也是因为如此,Python的效率水平无法满足该程序的算力要求,所以后续亟待转用C++重写。
程序使用全屏模式显示,全局常量SIZE
数对表示视窗的水平长度和垂直长度。在Windows平台下,程序会自动尝试获取显示屏的分辨率,作为全屏显示的尺寸。在其他平台下,程序使用默认的分辨率,即1366 * 768
(我的电脑最高分辨率是这样的,不同分辨率的电脑可以自行修改)。
项目未使用贴图的方式,渲染的图像渐变是根据内置绘制方法实现的。
待改进
- 由于目前只是自己测试来玩的,所以写得比较随性,有些代码和变量可能没有给出具体的解释,请见谅,日后会发布完善的新版本。
- 算力性能限制,原因已经说了,是语言效率问题。
- 当前版本不是批量引力模拟,所以采用很多变量,而不是用对象来代表星体,理由同上,仅为了测试。
项目代码
GitHub仓库内代码在此
请注意:该项目使用PyGame作为图形界面,采用全屏显示。
以下是具体代码:
# 引入库
import pygame, math, sys, random, ctypes
pygame.init()
# 常量
try:
GetSystemMetrics = ctypes.windll.user32.GetSystemMetrics
SIZE = [GetSystemMetrics(0), GetSystemMetrics(1)]
except:
SIZE = [1366, 768]
tps = 1 # 模拟器中的时间(秒)与现实时间(秒)之比
fps = 10 # 刷新率
FILL = True
# 以下常量是游戏中的时间常量
uptime = 1000 / fps # 游戏刷新间隔(毫秒)
def getr(m):
return m ** 0.3 * 6#math.log(m1*80) * 5
def reset_star1():
global m1, r1, r2, obj1, obj2, speed1
m1 = random.randint(1, 10000)#random.randint(50, 1600)
r1 = getr(m1)
obj1 = [random.randint(0 + int(r1), SIZE[0] - int(r1)), random.randint(0+int(r1), SIZE[1]-int(r1))]
distx = obj1[0] - obj2[0]
disty = obj1[1] - obj2[1]
dist = math.sqrt(distx**2+disty**2)
if dist < (r1+r2) * 1.5: reset_star1(); return
speed1 = [random.randint(-400, 400), random.randint(-400, 400)]
# 测试用数据
m2 = random.randint(20, 1000)
r2 = getr(m2)
obj2 = [random.randint(0 + int(r2), SIZE[0]-int(r2)), random.randint(0+int(r2), SIZE[1]-int(r2))]
speed2 = [random.randint(-400, 400), random.randint(-400, 400)]
reset_star1()
c1 = [242, 112, 34]
c2 = [154,41,133]
c3 = [125, 39, 236]
c7 = [255, 142, 255]
c4 = [18, 20, 64]
c5 = [36, 39, 128]
c6 = [151, 196, 255]
c8 = [207, 224, 234]
BACKGROUND = [6, 3, 24]
filler1 = c4[:]
filler1[0] //= 2
filler1[1] //= 2
filler1[2] //= 2
g = 20
# 函数
last = r1+r2
def fill(center, rs, re, cs, ce): # 绘制环形渐变,rs > re
'''默认为20层渐变'''
pygame.draw.circle(s, cs, center, rs)
dr = (re - rs) / 20
dcr = (ce[0] - cs[0]) / 20
dcg = (ce[1] - cs[1]) / 20
dcb = (ce[2] - cs[2]) / 20
for i in range(1, 19):
pygame.draw.circle(s, (int(cs[0] + dcr*i), int(cs[1] + dcg*i), int(cs[2] + dcb*i)), center, int(rs + dr*i))
pygame.draw.circle(s, ce, center, re)
def t(num):
if num > 0: return 1
if num < 0: return -1
return 0
def set_force(): # 处理obj1受到obj2的引力
global last, m1, m2, r1, r2, speed1, speed2
distx = obj1[0] - obj2[0]
disty = obj1[1] - obj2[1]
dist = math.sqrt(distx**2+disty**2)
if r1+r2+10 > dist: # 两星体相撞,目前默认由星体2号吸收1号
#if dist < min(r1, r2):
# speed1, speed2 = [0, 0], [0, 0] # 这里改成使用last的表达式
s = (r1+r2 - dist) / 4# 粗略计算重合的部分线段长
# 目标:将距离s一部分留给1号,另一部分被2号吸收,并最终使两星体相切,即r1+r2==dist
#d1 = ((m1-m2) + math.sqrt((m1-m2)**2 + min(dist, m1))) / 2
rawdm = 0.5*s * (r1+r2) if 0.5*s * (r1+r2) < m1 else m1 # 粗略计算被吸收的物质质量
dm = 0
#m2 += dm; m1 -= dm
#newm1, newm2 = m1, m2
while m1 >= 0 and m2 >= 0 and dm <= rawdm * 4:
m2 += 1; m1 -= 1; dm += 1
if not (m1 >= 0 and m2 >= 0): break
r1 = getr(m1); r2 = getr(m2)
if abs(dist-(r1+r2)) <= 5: break
if m1 < 0: m1 = 0
ns2 = []
#ns1, ns2 = [], []
#ns1.append((speed1[0]*m1 + speed2[0]*dm/max(m1, 1)) / (m1+dm))
#ns1.append((speed1[1]*m1 + speed2[1]*dm/max(m1, 1)) / (m1+dm))
ns2.append((speed2[0]*m2 + speed1[0]*dm) / (m2+dm))
ns2.append((speed2[1]*m2 + speed1[1]*dm) / (m2+dm))
#ns2.append((speed2[0]*m2 + speed1[0]*dm/max(m1, 1)) / (m2+dm))
#ns2.append((speed2[1]*m2 + speed1[1]*dm/max(m1, 1)) / (m2+dm))
speed2 = ns2
#speed1, speed2 = ns1, ns2
r1 = getr(m1) if m1 > 0 else 0
r2 = getr(m2) if m2 > 0 else 0
#print(m1, m2, m1+m2)
#if r1 < 0: return 'quit'
if r1 + r2 > 1.5*dist:
f = int(g * m1 * m2 / last**2)
else:
f = int(g * m1 * m2 / max(dist, last)**2)
last = dist
if obj2[0] + r2 > SIZE[0]: speed2[0] = -abs(speed2[0]); obj2[0] = SIZE[0] - r2
if obj2[0] - r2 < 0: speed2[0] = abs(speed2[0]); obj2[0] = r2
if obj2[1] + r2> SIZE[1]: speed2[1] = -abs(speed2[1]); obj2[1] = SIZE[1] - r2
if obj2[1] - r2< 0: speed2[1] = abs(speed2[1]); obj2[1] = r2
#if dist <= r1+r2:
# speed2[0] += last*t(distx) / m2
# speed2[1] += last*t(disty) / m2
#else:
if m2:
speed2[0] += f*t(distx) / m2 * tps
speed2[1] += f*t(disty) / m2 * tps
obj2[0] += speed2[0] / uptime * tps
obj2[1] += speed2[1] / uptime * tps
if obj1[0] + r1> SIZE[0]: speed1[0] = -abs(speed1[0]); obj1[0] = SIZE[0] - r1
if obj1[0] - r1< 0: speed1[0] = abs(speed1[0]); obj1[0] = r1
if obj1[1] + r1> SIZE[1]: speed1[1] = -abs(speed1[1]); obj1[1] = SIZE[1] - r1
if obj1[1] - r1< 0: speed1[1] = abs(speed1[1]); obj1[1] = r1
#if dist <= r1+r2:
# speed1[0] += -last*t(distx) / m1
# speed1[1] += -last*t(disty) / m1
#else:
if m1:
speed1[0] += -f*t(distx) / m1 * tps
speed1[1] += -f*t(disty) / m1 * tps
obj1[0] += speed1[0] / uptime * tps
obj1[1] += speed1[1] / uptime * tps
#last = f
#print(distx, disty, obj1, obj2)
# 主程序
s = pygame.display.set_mode(SIZE, pygame.RESIZABLE | pygame.FULLSCREEN)
clock = pygame.time.Clock()
running = True
shotcnt = 0
while running:
for i in pygame.event.get():
if i.type == pygame.QUIT:
running = False
elif i.type == pygame.KEYDOWN:
keys = pygame.key.get_pressed()
if keys[pygame.K_ESCAPE]:
running = False
if keys[pygame.K_LEFT]:
m2 = m2 * 0.9
r2 = getr(m2)
if keys[pygame.K_RIGHT]:
m2 = m2 * 1.1
r2 = getr(m2)
if keys[pygame.K_a]:
m1 = m1 * 0.9
r1 = getr(m1)
if keys[pygame.K_d]:
m1 = m1 * 1.1
r1 = getr(m1)
if keys[pygame.K_s]:
m2 = m1
r2 = getr(m2)
clock.tick(uptime)
if FILL: s.fill(BACKGROUND)
#if r1 < 0: break
#pygame.draw.rect(s, (0, 0, 0), [0, 0, SIZE[0], SIZE[1]])
pos1 = [int(obj1[0] + 0.5), int (obj1[1] + 0.5)]
pos2 = [int(obj2[0] + 0.5), int (obj2[1] + 0.5)]
if m1:
fill(pos1, abs(int(r1)), abs(int(r1 / 1.1)), c4, c5)
fill(pos1, abs(int(r1 / 1.1)), abs(int(r1 / 2)), c5, c6)
#pygame.draw.circle(s, c4, pos1, abs(int(r1)))
#pygame.draw.circle(s, c5, pos1, abs(int(r1 / 1.1)))
#pygame.draw.circle(s, c6, pos1, abs(int(r1 / 1.5)))
fill(pos2, int(r2), int(r2 * 0.75), filler1, c1)
# fill(pos2, int(r2), int(r2 * 3 / 4), c1, c2)
fill(pos2, int(r2 * 0.75), int(r2 / 2.5), c2, c3)
#fill(pos2, int(r2 / 2.5), int(r2 / 16), c3, c7)
#pygame.draw.circle(s, c1, pos2, int(r2))
#pygame.draw.circle(s, c2, pos2, int(r2 * 3 / 4))
pygame.draw.circle(s, c3, pos2, int(r2 / 2.5))
pygame.draw.circle(s, c7, pos2, int(r2 / 16))
pygame.display.flip()
if set_force() == 'quit':
running = False
if m1 == 0: # 重设一次
reset_star1()
#if m2 <= 1:
# print("Sun shrinked out!")
# break
#m2 *= 0.995
#m1 *= 1.005
#r1 = getr(m1)
#r2 = getr(m2)
pygame.quit()
计划
计划将这个项目开发成一个不同于Osmos游戏的引力模拟器,更加专注于真实模拟。
计划用C++语言改写为引力批量模拟模式。目前在和Osmos的开发者交流学习引力模拟的知识。很感谢这位开发者的耐心指导。渲染的图形效果灵感来自于Osmos中的星体渲染。
如果有懂引力模拟相关知识的大佬或者对引力模拟有兴趣的同学,欢迎指教或改进代码。
谢谢!