Added water splash simulator, made settings saving work for all simulators, moved all the simulators into their respective directories, improved the README, fixed too much logging

This commit is contained in:
csd4ni3l
2025-09-14 22:27:54 +02:00
parent 9c632d1bec
commit c010c41ccd
12 changed files with 395 additions and 54 deletions

1
.gitignore vendored
View File

@@ -180,3 +180,4 @@ test*.py
logs/ logs/
logs logs
settings.json settings.json
data.json

View File

@@ -1 +1,5 @@
Some simulator games i tried to make to get better at math and interesting concepts. Currently includes a Boids simulator and a Physics Sandbox Some simulator games i tried to make to get better at math and interesting concepts. Currently includes a Boids simulator, Water Simulator and a Physics Sandbox
The Water Simulator simulates 2d water splashes.
The Physics Sandbox includes crates, coins, and any custom SVG you want! Use w to place the current inventory item, and on the right bottom you can select new ones.
The boids simulator simulates how birds move together and you can change the weights so they move in a certain direction!

Binary file not shown.

Before

Width:  |  Height:  |  Size: 338 B

After

Width:  |  Height:  |  Size: 264 B

View File

@@ -1,6 +1,6 @@
import arcade, arcade.gui, random import arcade, arcade.gui, random, os, json
from game.boid import Boid from game.boid_simulator.boid import Boid
class Game(arcade.gui.UIView): class Game(arcade.gui.UIView):
def __init__(self, pypresence_client): def __init__(self, pypresence_client):
@@ -11,6 +11,21 @@ class Game(arcade.gui.UIView):
self.boid_sprites = arcade.SpriteList() self.boid_sprites = arcade.SpriteList()
self.current_boid_num = 1 self.current_boid_num = 1
if os.path.exists("data.json"):
with open("data.json", "r") as file:
self.settings = json.load(file)
else:
self.settings = {}
if not "boid_simulator" in self.settings:
self.settings["boid_simulator"] = {
"w_separation": 1.0,
"w_alignment": 1.0,
"w_cohesion": 1.0,
"small_radius": 100,
"large_radius": 250
}
self.anchor = self.add_widget(arcade.gui.UIAnchorLayout(size_hint=(1, 1))) self.anchor = self.add_widget(arcade.gui.UIAnchorLayout(size_hint=(1, 1)))
self.settings_box = self.anchor.add(arcade.gui.UIBoxLayout(space_between=5, align="center", size_hint=(0.2, 1)).with_background(color=arcade.color.GRAY), anchor_x="right", anchor_y="bottom") self.settings_box = self.anchor.add(arcade.gui.UIBoxLayout(space_between=5, align="center", size_hint=(0.2, 1)).with_background(color=arcade.color.GRAY), anchor_x="right", anchor_y="bottom")
@@ -18,12 +33,16 @@ class Game(arcade.gui.UIView):
self.add_setting("Separation Weight: {value}", 0.1, 5, 0.1, "w_separation") self.add_setting("Separation Weight: {value}", 0.1, 5, 0.1, "w_separation")
self.add_setting("Alignment Weight: {value}", 0.1, 5, 0.1, "w_alignment") self.add_setting("Alignment Weight: {value}", 0.1, 5, 0.1, "w_alignment")
self.add_setting("Cohesion Weight: {value}", 0.1, 5, 0.1, "w_cohesion") self.add_setting("Cohesion Weight: {value}", 0.1, 5, 0.1, "w_cohesion")
self.add_setting("Small Radius: {value}", 25, 250, 25, "small_radius", 100) self.add_setting("Small Radius: {value}", 25, 250, 25, "small_radius")
self.add_setting("Large Radius: {value}", 50, 500, 50, "large_radius", 250) self.add_setting("Large Radius: {value}", 50, 500, 50, "large_radius")
def add_setting(self, text, min_value, max_value, step, boid_variable, default=None): def save_data(self):
label = self.settings_box.add(arcade.gui.UILabel(text.format(value=default or 1.0))) with open("data.json", "w") as file:
slider = self.settings_box.add(arcade.gui.UISlider(value=1.0, min_value=min_value, max_value=max_value, step=step, size_hint=(1, 0.05))) file.write(json.dumps(self.settings, indent=4))
def add_setting(self, text, min_value, max_value, step, boid_variable):
label = self.settings_box.add(arcade.gui.UILabel(text.format(value=self.settings["boid_simulator"][boid_variable])))
slider = self.settings_box.add(arcade.gui.UISlider(value=self.settings["boid_simulator"][boid_variable], min_value=min_value, max_value=max_value, step=step, size_hint=(1, 0.05)))
slider._render_steps = lambda surface: None slider._render_steps = lambda surface: None
slider.on_change = lambda event, label=label: self.change_value(label, text, boid_variable, event.new_value) slider.on_change = lambda event, label=label: self.change_value(label, text, boid_variable, event.new_value)
@@ -31,6 +50,8 @@ class Game(arcade.gui.UIView):
def change_value(self, label, text, boid_variable, value): def change_value(self, label, text, boid_variable, value):
label.text = text.format(value=value) label.text = text.format(value=value)
self.settings["boid_simulator"][boid_variable] = value
for boid in self.boid_sprites: for boid in self.boid_sprites:
setattr(boid, boid_variable, value) setattr(boid, boid_variable, value)
@@ -57,6 +78,8 @@ class Game(arcade.gui.UIView):
def on_key_press(self, symbol, modifiers): def on_key_press(self, symbol, modifiers):
if symbol == arcade.key.ESCAPE: if symbol == arcade.key.ESCAPE:
self.save_data()
from menus.main import Main from menus.main import Main
self.window.show_view(Main(self.pypresence_client)) self.window.show_view(Main(self.pypresence_client))

View File

@@ -11,7 +11,7 @@ class BodyInventory(arcade.gui.UIGridLayout):
n = 0 n = 0
for name, image in items.items(): for name, image in items.items():
self.buttons[name] = self.add(arcade.gui.UITextureButton(width=(window_width * 0.2) / 4, height=(window_width * 0.2) / 4).with_background(texture=arcade.load_texture(image), color=arcade.color.WHITE if name == self.selected_item else arcade.color.TRANSPARENT_BLACK), column=n % 2, row=n // 2) self.buttons[name] = self.add(arcade.gui.UITextureButton(width=(window_width * 0.2) / 4, height=(window_width * 0.2) / 4).with_background(texture=arcade.load_texture(image), color=arcade.color.WHITE if name == self.selected_item else arcade.color.TRANSPARENT_BLACK).with_border(color=arcade.color.WHITE), column=n % 2, row=n // 2)
self.buttons[name].on_click = lambda event, name=name: self.change_to(name) self.buttons[name].on_click = lambda event, name=name: self.change_to(name)
n += 1 n += 1
@@ -22,11 +22,11 @@ class BodyInventory(arcade.gui.UIGridLayout):
self._update_size_hints() self._update_size_hints()
self.items[name] = image self.buttons[name] = self.add(arcade.gui.UITextureButton(width=(self.window_width * 0.2) / 4, height=(self.window_width * 0.2) / 4, color=arcade.color.TRANSPARENT_BLACK).with_background(texture=image).with_border(color=arcade.color.WHITE), column=len(self.items) % 2, row=len(self.items) // 2)
self.buttons[name] = self.add(arcade.gui.UITextureButton(width=(self.window_width * 0.2) / 2, height=(self.window_height * 0.1) / math.ceil(len(self.items) / 2), color=arcade.color.TRANSPARENT_BLACK).with_background(texture=image), column=len(self.items) % 2, row=len(self.items) // 2)
self.buttons[name].on_click = lambda event, name=name: self.change_to(name) self.buttons[name].on_click = lambda event, name=name: self.change_to(name)
self.items[name] = image
def change_to(self, name): def change_to(self, name):
self.buttons[self.selected_item] = self.buttons[self.selected_item].with_background(color=arcade.color.TRANSPARENT_BLACK) self.buttons[self.selected_item] = self.buttons[self.selected_item].with_background(color=arcade.color.TRANSPARENT_BLACK)
self.selected_item = name self.selected_item = name

View File

@@ -1,22 +1,31 @@
import arcade, arcade.gui, pymunk, pymunk.util, math, time, os, io, cairosvg import arcade, arcade.gui, pymunk, pymunk.util, math, time, os, io, cairosvg, json, random
from PIL import Image from PIL import Image
from arcade.gui.experimental.scroll_area import UIScrollArea, UIScrollBar from arcade.gui.experimental.scroll_area import UIScrollArea, UIScrollBar
from pymunk.autogeometry import convex_decomposition
from svgpathtools import svg2paths from svgpathtools import svg2paths
from game.body_inventory import BodyInventory
from game.physics_playground.body_inventory import BodyInventory
from utils.constants import menu_background_color, button_style from utils.constants import menu_background_color, button_style
from utils.preload import button_texture, button_hovered_texture from utils.preload import button_texture, button_hovered_texture
class FakeShape():
def __init__(self, body):
self.body = body
class CustomPhysics(arcade.Sprite):
def __init__(self, pymunk_obj, filename):
super().__init__(filename, center_x=pymunk_obj.body.position.x, center_y=pymunk_obj.body.position.y)
self.pymunk_obj = pymunk_obj
class SpritePhysics(arcade.Sprite): class SpritePhysics(arcade.Sprite):
def __init__(self, pymunk_obj, filename): def __init__(self, pymunk_obj, filename):
super().__init__(filename, center_x=pymunk_obj.body.position.x, center_y=pymunk_obj.body.position.y) super().__init__(filename, center_x=pymunk_obj.body.position.x, center_y=pymunk_obj.body.position.y)
self.pymunk_obj = pymunk_obj self.pymunk_obj = pymunk_obj
self.origin_x = 0
self.origin_y = 0
class PhysicsCoin(SpritePhysics): class PhysicsCoin(SpritePhysics):
def __init__(self, pymunk_obj, filename): def __init__(self, pymunk_obj, filename):
super().__init__(pymunk_obj, filename) super().__init__(pymunk_obj, filename)
@@ -38,10 +47,36 @@ class Game(arcade.gui.UIView):
arcade.set_background_color(arcade.color.WHITE) arcade.set_background_color(arcade.color.WHITE)
if os.path.exists("data.json"):
with open("data.json", "r") as file:
self.settings = json.load(file)
else:
self.settings = {}
if not "physics_playground" in self.settings:
self.settings["physics_playground"] = {
"iterations": 50,
"gravity_x": 0,
"gravity_y": -930,
"crate_elasticity": 0.5,
"crate_friction": 0.9,
"crate_mass": 1,
"coin_elasticity": 0.5,
"coin_friction": 0.9,
"coin_mass": 1,
"custom_elasticity": 0.5,
"custom_friction": 0.9,
"custom_mass": 1
}
self.space = pymunk.Space() self.space = pymunk.Space()
self.spritelist: arcade.SpriteList[SpritePhysics] = arcade.SpriteList() self.spritelist: arcade.SpriteList[SpritePhysics] = arcade.SpriteList()
self.walls = [] self.walls = []
self.custom_bodies = []
self.custom_pymunk_objs = {} self.custom_pymunk_objs = {}
@@ -49,20 +84,24 @@ class Game(arcade.gui.UIView):
self.last_mouse_position = 0, 0 self.last_mouse_position = 0, 0
self.last_processing_time_update = time.perf_counter() self.last_processing_time_update = time.perf_counter()
self.iterations = 35 self.iterations = self.settings["physics_playground"].get("iterations", 35)
self.space.iterations = self.iterations self.space.iterations = self.iterations
self.gravity_x = 0 self.gravity_x = self.settings["physics_playground"].get("gravity_x", 0)
self.gravity_y = -900 self.gravity_y = self.settings["physics_playground"].get("gravity_y", -930)
self.space.gravity = (self.gravity_x, self.gravity_y) self.space.gravity = (self.gravity_x, self.gravity_y)
self.crate_elasticity = 0.5 self.crate_elasticity = self.settings["physics_playground"].get("crate_elasticity", 0.5)
self.crate_friction = 0.9 self.crate_friction = self.settings["physics_playground"].get("crate_friction", 0.9)
self.crate_mass = 1 self.crate_mass = self.settings["physics_playground"].get("crate_mass", 1)
self.coin_elasticity = 0.5 self.coin_elasticity = self.settings["physics_playground"].get("coin_elasticity", 0.5)
self.coin_friction = 0.9 self.coin_friction = self.settings["physics_playground"].get("coin_friction", 0.9)
self.coin_mass = 1 self.coin_mass = self.settings["physics_playground"].get("coin_mass", 1)
self.custom_elasticity = self.settings["physics_playground"].get("custom_elasticity", 0.5)
self.custom_friction = self.settings["physics_playground"].get("custom_friction", 0.9)
self.custom_mass = self.settings["physics_playground"].get("custom_mass", 1)
self.anchor = self.add_widget(arcade.gui.UIAnchorLayout(size_hint=(1, 1))) self.anchor = self.add_widget(arcade.gui.UIAnchorLayout(size_hint=(1, 1)))
@@ -71,26 +110,43 @@ class Game(arcade.gui.UIView):
self.object_count_label = self.info_box.add(arcade.gui.UILabel(text="Object count: 0", text_color=arcade.color.BLACK)) self.object_count_label = self.info_box.add(arcade.gui.UILabel(text="Object count: 0", text_color=arcade.color.BLACK))
self.processing_time_label = self.info_box.add(arcade.gui.UILabel(text="Processing time: 0 ms", text_color=arcade.color.BLACK)) self.processing_time_label = self.info_box.add(arcade.gui.UILabel(text="Processing time: 0 ms", text_color=arcade.color.BLACK))
self.settings_box = self.anchor.add(arcade.gui.UIBoxLayout(space_between=5, align="center", size_hint=(0.2, 1)).with_background(color=arcade.color.GRAY), anchor_x="right", anchor_y="bottom") self.settings_box = self.anchor.add(arcade.gui.UIBoxLayout(align="center", size_hint=(0.2, 1)).with_background(color=arcade.color.GRAY), anchor_x="right", anchor_y="bottom")
self.settings_title_label = self.settings_box.add(arcade.gui.UILabel(text="Settings", font_size=24)) self.settings_title_label = self.settings_box.add(arcade.gui.UILabel(text="Settings", font_size=24))
self.settings_box.add(arcade.gui.UISpace(size_hint=(0, 0.025))) self.settings_box.add(arcade.gui.UISpace(size_hint=(0, 0.025)))
self.add_setting("Crate Elasticity: {value}", 0, 3, 0.1, "crate_elasticity", "elasticity", PhysicsCrate) self.add_setting("Crate Elasticity: {value}", 0, 3, 0.1, "crate_elasticity", "elasticity", PhysicsCrate)
self.add_setting("Coin Elasticity: {value}", 0, 3, 0.1, "coin_elasticity", "elasticity", PhysicsCoin) self.add_setting("Coin Elasticity: {value}", 0, 3, 0.1, "coin_elasticity", "elasticity", PhysicsCoin)
self.add_setting("Custom Elasticity: {value}", 0, 3, 0.1, "custom_elasticity", "elasticity", CustomPhysics)
self.add_setting("Crate Friction: {value}", 0, 10, 0.1, "crate_friction", "friction", PhysicsCrate) self.add_setting("Crate Friction: {value}", 0, 10, 0.1, "crate_friction", "friction", PhysicsCrate)
self.add_setting("Coin Friction: {value}", 0, 10, 0.1, "coin_friction", "friction", PhysicsCoin) self.add_setting("Coin Friction: {value}", 0, 10, 0.1, "coin_friction", "friction", PhysicsCoin)
self.add_setting("Custom Friction: {value}", 0, 10, 0.1, "custom_friction", "friction", CustomPhysics)
self.add_setting("Crate Mass: {value}kg", 1, 100, 1, "crate_mass", "mass", PhysicsCrate) self.add_setting("Crate Mass: {value}kg", 1, 100, 1, "crate_mass", "mass", PhysicsCrate)
self.add_setting("Coin Mass: {value}kg", 1, 100, 1, "coin_mass", "mass", PhysicsCoin) self.add_setting("Coin Mass: {value}kg", 1, 100, 1, "coin_mass", "mass", PhysicsCoin)
self.add_setting("Custom Mass: {value}kg", 1, 100, 1, "custom_mass", "mass", CustomPhysics)
self.add_setting("Gravity X: {value}", -900, 900, 100, "gravity_x", on_change=lambda label, value: self.change_gravity(label, value, "x")) self.add_setting("Gravity X: {value}", -900, 900, 100, "gravity_x", on_change=lambda label, value: self.change_gravity(label, value, "x"))
self.add_setting("Gravity Y: {value}", -1800, 1800, 100, "gravity_y", on_change=lambda label, value: self.change_gravity(label, value, "y")) self.add_setting("Gravity Y: {value}", -1800, 1800, 100, "gravity_y", on_change=lambda label, value: self.change_gravity(label, value, "y"))
self.add_setting("Pymunk Iterations: {value}", 1, 200, 1, "iterations", on_change=lambda label, value: self.change_iterations(label, value))
self.settings_box.add(arcade.gui.UILabel("Inventory", font_size=18))
self.inventory_grid = self.settings_box.add(BodyInventory(self.window.width, self.window.height, "crate", {"crate": ":resources:images/tiles/boxCrate_double.png", "coin": ":resources:images/items/coinGold.png"})) self.inventory_grid = self.settings_box.add(BodyInventory(self.window.width, self.window.height, "crate", {"crate": ":resources:images/tiles/boxCrate_double.png", "coin": ":resources:images/items/coinGold.png"}))
self.add_custom_body_button = self.settings_box.add(arcade.gui.UITextureButton(text="Add custom body from SVG", size_hint=(1, 0.1), width=self.window.width * 0.2, height=self.window.height * 0.1)) self.add_custom_body_button = self.settings_box.add(arcade.gui.UITextureButton(text="Add custom body from SVG", size_hint=(1, 0.1), width=self.window.width * 0.2, height=self.window.height * 0.1))
self.add_custom_body_button.on_click = lambda event: self.custom_body_ui() self.add_custom_body_button.on_click = lambda event: self.custom_body_ui()
def save_data(self):
with open("data.json", "w") as file:
file.write(json.dumps(self.settings, indent=4))
def change_iterations(self, label, value):
self.iterations = int(value)
self.space.iterations = self.iterations
label.text = f"Pymunk Iterations: {self.iterations}"
def change_gravity(self, label, value, gravity_type): def change_gravity(self, label, value, gravity_type):
if gravity_type == "x": if gravity_type == "x":
self.gravity_x = value self.gravity_x = value
@@ -117,6 +173,8 @@ class Game(arcade.gui.UIView):
setattr(self, local_variable, value) setattr(self, local_variable, value)
self.settings["physics_playground"][local_variable] = value
if pymunk_variable: if pymunk_variable:
for sprite in self.spritelist: for sprite in self.spritelist:
if isinstance(sprite, instance): if isinstance(sprite, instance):
@@ -131,7 +189,7 @@ class Game(arcade.gui.UIView):
self.walls.append(pymunk_obj) self.walls.append(pymunk_obj)
def create_crate(self, x, y, size, mass): def create_crate(self, x, y, size, mass):
pymunk_moment = pymunk.moment_for_box(mass, (size, size)) pymunk_moment = pymunk.moment_for_box(1.0, (size, size))
pymunk_body = pymunk.Body(mass, pymunk_moment) pymunk_body = pymunk.Body(mass, pymunk_moment)
pymunk_body.position = pymunk.Vec2d(x, y) pymunk_body.position = pymunk.Vec2d(x, y)
@@ -146,7 +204,7 @@ class Game(arcade.gui.UIView):
self.spritelist.append(sprite) self.spritelist.append(sprite)
def create_coin(self, x, y, radius, mass): def create_coin(self, x, y, radius, mass):
inertia = pymunk.moment_for_circle(mass, 0, radius, (0, 0)) inertia = pymunk.moment_for_circle(1.0, 0, radius, (0, 0))
body = pymunk.Body(mass, inertia) body = pymunk.Body(mass, inertia)
body.position = x, y body.position = x, y
@@ -173,6 +231,12 @@ class Game(arcade.gui.UIView):
arcade.draw_line(pv1.x, pv1.y, pv2.x, pv2.y, arcade.color.BLACK, 2) arcade.draw_line(pv1.x, pv1.y, pv2.x, pv2.y, arcade.color.BLACK, 2)
for body in self.custom_bodies:
for shape in body.shapes:
if isinstance(shape, pymunk.Poly):
verts = [v.rotated(body.angle) + body.position for v in shape.get_vertices()]
arcade.draw_polygon_filled(verts, arcade.color.BLACK)
def on_mouse_press(self, x, y, button, modifiers): def on_mouse_press(self, x, y, button, modifiers):
if button == arcade.MOUSE_BUTTON_LEFT: if button == arcade.MOUSE_BUTTON_LEFT:
self.last_mouse_position = x, y self.last_mouse_position = x, y
@@ -219,7 +283,7 @@ class Game(arcade.gui.UIView):
def add_custom_body(self, file_path): def add_custom_body(self, file_path):
paths, _ = svg2paths(file_path) paths, _ = svg2paths(file_path)
pts = self.sample_path(paths[0], 15) pts = self.sample_path(paths[0], 64)
png_bytes = cairosvg.svg2png(url=file_path, scale=1.0) png_bytes = cairosvg.svg2png(url=file_path, scale=1.0)
original_image = Image.open(io.BytesIO(png_bytes)).convert("RGBA") original_image = Image.open(io.BytesIO(png_bytes)).convert("RGBA")
@@ -229,32 +293,36 @@ class Game(arcade.gui.UIView):
scale_factor = desired_width / original_width scale_factor = desired_width / original_width
pts = [(x * scale_factor, y * scale_factor) for x, y in pts] pts = [(x * scale_factor, y * scale_factor) for x, y in pts]
try:
convex_parts = convex_decomposition(pts, 0.1)
except AssertionError:
convex_parts = [pymunk.util.convex_hull(pts)]
hull = pymunk.util.convex_hull(pts) total_moment = sum(pymunk.moment_for_poly(1.0, part) for part in convex_parts)
moment = pymunk.moment_for_poly(1.0, hull)
png_bytes = cairosvg.svg2png(url=file_path, scale=scale_factor) png_bytes = cairosvg.svg2png(url=file_path, scale=scale_factor)
image = Image.open(io.BytesIO(png_bytes)).convert("RGBA") image = Image.open(io.BytesIO(png_bytes)).convert("RGBA")
texture = arcade.Texture(image) texture = arcade.Texture(image)
self.custom_pymunk_objs[file_path] = (hull, moment, texture) self.custom_pymunk_objs[file_path] = (convex_parts, total_moment, texture)
self.inventory_grid.add_item(file_path, texture) self.inventory_grid.add_item(file_path, texture)
self.clear_custom_body_ui() self.clear_custom_body_ui()
def create_custom_body(self, file_path, x, y, mass): def create_custom_body(self, file_path, x, y, mass):
hull, moment, image = self.custom_pymunk_objs[file_path] convex_parts, moment, image = self.custom_pymunk_objs[file_path]
body = pymunk.Body(mass, moment) body = pymunk.Body(mass, moment)
body.position = pymunk.Vec2d(x, y) body.position = pymunk.Vec2d(x, y)
shape = pymunk.Poly(body, hull) self.space.add(body)
self.space.add(body, shape)
sprite = SpritePhysics(shape, image) for part in convex_parts:
sprite.origin_x = image.width / 2 shape = pymunk.Poly(body, part)
sprite.origin_y = image.height / 2 self.space.add(shape)
sprite = CustomPhysics(FakeShape(body), image)
self.spritelist.append(sprite) self.spritelist.append(sprite)
@@ -326,13 +394,15 @@ class Game(arcade.gui.UIView):
elif self.inventory_grid.selected_item == "coin": elif self.inventory_grid.selected_item == "coin":
self.create_coin(self.window.mouse.data['x'], self.window.mouse.data['y'], 10, self.coin_mass) self.create_coin(self.window.mouse.data['x'], self.window.mouse.data['y'], 10, self.coin_mass)
else: else:
self.create_custom_body(self.inventory_grid.selected_item, self.window.mouse.data['x'], self.window.mouse.data['y'], 1.0) self.create_custom_body(self.inventory_grid.selected_item, self.window.mouse.data['x'], self.window.mouse.data['y'], self.custom_mass)
for sprite in self.spritelist: for sprite in self.spritelist:
if sprite.pymunk_obj.body.position.x < 0 or sprite.pymunk_obj.body.position.x > self.window.width * 0.8 or sprite.pymunk_obj.body.position.y < 0: body = sprite.pymunk_obj.body
self.space.remove(sprite.pymunk_obj, sprite.pymunk_obj.body) x, y = body.position
sprite.remove_from_sprite_lists() if x < 0 or x > self.window.width * 0.775 or y < 0:
body.position = (random.uniform(self.window.width * 0.1, self.window.width * 0.9), self.window.height * 0.9)
body.velocity = (0, 0)
start = time.perf_counter() start = time.perf_counter()
self.space.step(self.window._draw_rate) self.space.step(self.window._draw_rate)
@@ -342,8 +412,7 @@ class Game(arcade.gui.UIView):
self.dragged_shape.shape.body.velocity = 0, 0 self.dragged_shape.shape.body.velocity = 0, 0
for sprite in self.spritelist: for sprite in self.spritelist:
sprite.center_x = sprite.pymunk_obj.body.position.x + sprite.origin_x sprite.position = sprite.pymunk_obj.body.position
sprite.center_y = sprite.pymunk_obj.body.position.y + sprite.origin_y
sprite.angle = -math.degrees(sprite.pymunk_obj.body.angle) sprite.angle = -math.degrees(sprite.pymunk_obj.body.angle)
self.object_count_label.text = f"Object count: {len(self.spritelist)}" self.object_count_label.text = f"Object count: {len(self.spritelist)}"
@@ -356,13 +425,21 @@ class Game(arcade.gui.UIView):
def on_key_press(self, symbol, modifiers): def on_key_press(self, symbol, modifiers):
if symbol == arcade.key.ESCAPE: if symbol == arcade.key.ESCAPE:
arcade.set_background_color(menu_background_color) arcade.set_background_color(menu_background_color)
self.save_data()
from menus.main import Main from menus.main import Main
self.window.show_view(Main(self.pypresence_client)) self.window.show_view(Main(self.pypresence_client))
elif symbol == arcade.key.D: elif symbol == arcade.key.D:
self.create_wall((self.window.width * 0.8) / 10, 80, self.window.mouse.data["x"] - (self.window.width * 0.8) / 20, self.window.mouse.data["y"] - 80) self.create_wall((self.window.width * 0.8) / 10, 80, self.window.mouse.data["x"] - (self.window.width * 0.8) / 20, self.window.mouse.data["y"] - 80)
elif symbol == arcade.key.C: elif symbol == arcade.key.C:
for sprite in self.spritelist: for sprite in self.spritelist:
self.space.remove(sprite.pymunk_obj, sprite.pymunk_obj.body) if not isinstance(sprite.pymunk_obj, FakeShape):
self.space.remove(sprite.pymunk_obj, sprite.pymunk_obj.body)
else:
for shape in sprite.pymunk_obj.body.shapes:
self.space.remove(shape)
self.space.remove(sprite.pymunk_obj.body)
self.spritelist.clear() self.spritelist.clear()

View File

@@ -0,0 +1,146 @@
import arcade, arcade.gui, pyglet.gl, array, random, os, json
from utils.constants import WATER_ROWS, WATER_COLS
from game.water_simulator.shader import create_shader
class Game(arcade.gui.UIView):
def __init__(self, pypresence_client):
super().__init__()
self.pypresence_client = pypresence_client
self.pypresence_client.update(state="Playing a simulator", details="Water Simulator")
self.anchor = self.add_widget(arcade.gui.UIAnchorLayout(size_hint=(1, 1)))
self.settings_box = self.anchor.add(arcade.gui.UIBoxLayout(align="center", size_hint=(0.2, 1)).with_background(color=arcade.color.GRAY), anchor_x="right", anchor_y="bottom")
self.settings_label = self.settings_box.add(arcade.gui.UILabel(text="Settings", font_size=24))
if os.path.exists("data.json"):
with open("data.json", "r") as file:
self.settings = json.load(file)
else:
self.settings = {}
if not "water_simulator" in self.settings:
self.settings["water_simulator"] = {
"splash_strength": 0.1,
"splash_radius": 3,
"wave_speed": 1,
"damping": 0.02
}
self.splash_row = 0
self.splash_col = 0
self.current_splash_strength = 0
self.splash_strength = self.settings["water_simulator"].get("splash_strength", 0.1)
self.splash_radius = self.settings["water_simulator"].get("splash_radius", 3)
self.wave_speed = self.settings["water_simulator"].get("wave_speed", 1)
self.damping = self.settings["water_simulator"].get("damping", 0.02)
def on_show_view(self):
super().on_show_view()
self.settings_box.add(arcade.gui.UISpace(height=self.window.height / 75))
self.add_setting("Splash Strength: {value}", 0.1, 2.0, 0.1, "splash_strength")
self.add_setting("Splash Radius: {value}", 0.5, 10, 0.5, "splash_radius")
self.settings_box.add(arcade.gui.UISpace(height=self.window.height / 50))
self.advanced_label = self.settings_box.add(arcade.gui.UILabel("Advanced Settings", font_size=18, multiline=True))
self.settings_box.add(arcade.gui.UISpace(height=self.window.height / 75))
self.add_setting("Wave Speed: {value}", 0.1, 1.25, 0.05, "wave_speed")
self.add_setting("Damping: {value}", 0.005, 0.05, 0.001, "damping")
self.setup_game()
def on_update(self, delta_time):
with self.shader_program:
self.shader_program["rows"] = WATER_ROWS
self.shader_program["cols"] = WATER_COLS
self.shader_program["splash_row"] = self.splash_row
self.shader_program["splash_col"] = self.splash_col
self.shader_program["splash_strength"] = self.current_splash_strength
self.shader_program["splash_radius"] = self.splash_radius
self.shader_program["wave_speed"] = self.wave_speed
self.shader_program["damping"] = self.damping
self.shader_program.dispatch(self.water_image.width, self.water_image.height, 1, barrier=pyglet.gl.GL_ALL_BARRIER_BITS)
self.current_splash_strength = 0
def save_data(self):
self.settings.update({
"water_simulator": {
"splash_strength": self.splash_strength,
"splash_radius": self.splash_radius,
"wave_speed": self.wave_speed,
"damping": self.damping
}
})
with open("data.json", "w") as file:
file.write(json.dumps(self.settings, indent=4))
def setup_game(self):
self.shader_program, self.water_image, self.previous_heights_ssbo, self.current_heights_ssbo = create_shader()
self.image_sprite = pyglet.sprite.Sprite(img=self.water_image)
scale_x = (self.window.width * 0.8) / self.image_sprite.width
scale_y = self.window.height / self.image_sprite.height
self.image_sprite.scale_x = scale_x
self.image_sprite.scale_y = scale_y
grid = array.array('f', [random.uniform(-0.01, 0.01) for _ in range(WATER_ROWS * WATER_COLS)])
self.previous_heights_ssbo.set_data(grid.tobytes())
self.current_heights_ssbo.set_data(grid.tobytes())
def add_setting(self, text, min_value, max_value, step, local_variable, on_change=None):
label = self.settings_box.add(arcade.gui.UILabel(text.format(value=getattr(self, local_variable))))
slider = self.settings_box.add(arcade.gui.UISlider(value=getattr(self, local_variable), min_value=min_value, max_value=max_value, step=step))
slider._render_steps = lambda surface: None
if on_change:
slider.on_change = lambda event, label=label: on_change(label, event.new_value)
else:
slider.on_change = lambda event, label=label: self.change_value(label, text, local_variable, event.new_value)
def change_value(self, label, text, local_variable, value):
label.text = text.format(value=value)
self.settings["water_simulator"][local_variable] = value
setattr(self, local_variable, value)
def main_exit(self):
self.shader_program.delete()
self.previous_heights_ssbo.delete()
self.current_heights_ssbo.delete()
def on_key_press(self, symbol, modifiers):
if symbol == arcade.key.ESCAPE:
self.save_data()
from menus.main import Main
self.window.show_view(Main(self.pypresence_client))
def on_mouse_press(self, x, y, button, modifiers):
col = int(x / (self.window.width * 0.8) * WATER_COLS)
row = int(y / self.window.height * WATER_ROWS)
self.splash_row = row
self.splash_col = col
self.current_splash_strength = self.splash_strength
def on_draw(self):
super().on_draw()
self.image_sprite.draw()

View File

@@ -0,0 +1,80 @@
import pyglet, pyglet.graphics
from pyglet.gl import glBindBufferBase, GL_SHADER_STORAGE_BUFFER, GL_NEAREST
from utils.constants import WATER_ROWS, WATER_COLS
shader_source = f"""#version 430 core
layout(std430, binding = 3) buffer PreviousHeights {{
float previous_heights[{WATER_ROWS * WATER_COLS}];
}};
layout(std430, binding = 4) buffer CurrentHeights {{
float current_heights[{WATER_ROWS * WATER_COLS}];
}};
uniform int rows;
uniform int cols;
uniform int splash_row;
uniform int splash_col;
uniform float damping;
uniform float wave_speed;
uniform float splash_strength;
uniform float splash_radius;
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
layout (location = 0, rgba32f) uniform image2D img_output;
void main() {{
ivec2 texel_coord = ivec2(gl_GlobalInvocationID.xy);
int row = texel_coord.y * rows / imageSize(img_output).y;
int col = texel_coord.x * cols / imageSize(img_output).x;
int current_index = (row * cols) + col;
if(row == 0 || col == 0 || row == rows-1 || col == cols-1) return;
float dist = distance(vec2(row, col), vec2(splash_row, splash_col));
if(dist <= splash_radius) current_heights[current_index] += splash_strength * (1.0 - dist / splash_radius);
float laplacian = current_heights[(row - 1) * cols + col] +
current_heights[(row + 1) * cols + col] +
current_heights[row * cols + (col - 1)] +
current_heights[row * cols + (col + 1)] -
4.0 * current_heights[current_index];
float dt = 0.1;
float h_new = 2.0 * current_heights[current_index]
- previous_heights[current_index] +
(wave_speed * wave_speed)*(dt*dt) * laplacian -
damping * (current_heights[current_index] - previous_heights[current_index]);
previous_heights[current_index] = current_heights[current_index];
current_heights[current_index] = h_new;
float minH = -0.5;
float maxH = 0.5;
float normH = clamp((h_new - minH) / (maxH - minH), 0.0, 1.0);
imageStore(img_output, texel_coord, vec4(0.0, 0.0, normH, 1.0));
}}
"""
def create_shader():
shader_program = pyglet.graphics.shader.ComputeShaderProgram(shader_source)
water_image = pyglet.image.Texture.create(WATER_COLS, WATER_ROWS, internalformat=pyglet.gl.GL_RGBA32F, min_filter=GL_NEAREST, mag_filter=GL_NEAREST)
uniform_location = shader_program['img_output']
water_image.bind_image_texture(unit=uniform_location)
previous_heights_ssbo = pyglet.graphics.BufferObject(WATER_COLS * WATER_ROWS * 4, usage=pyglet.gl.GL_DYNAMIC_COPY)
current_heights_ssbo = pyglet.graphics.BufferObject(WATER_COLS * WATER_ROWS * 4, usage=pyglet.gl.GL_DYNAMIC_COPY)
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, previous_heights_ssbo.id)
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 4, current_heights_ssbo.id)
return shader_program, water_image, previous_heights_ssbo, current_heights_ssbo

View File

@@ -52,21 +52,28 @@ class Main(arcade.gui.UIView):
self.title_label = self.box.add(arcade.gui.UILabel(text="Simulator Games", font_name="Roboto", font_size=48)) self.title_label = self.box.add(arcade.gui.UILabel(text="Simulator Games", font_name="Roboto", font_size=48))
self.boid_simulator_button = self.box.add(arcade.gui.UITextureButton(text="Boid Simulator", texture=button_texture, texture_hovered=button_hovered_texture, width=self.window.width / 2, height=150, style=big_button_style)) self.boid_simulator_button = self.box.add(arcade.gui.UITextureButton(text="Boid Simulator", texture=button_texture, texture_hovered=button_hovered_texture, width=self.window.width / 2, height=self.window.height / 8, style=big_button_style))
self.boid_simulator_button.on_click = lambda event: self.boid_simulator() self.boid_simulator_button.on_click = lambda event: self.boid_simulator()
self.physics_playground_button = self.box.add(arcade.gui.UITextureButton(text="Physics Playground", texture=button_texture, texture_hovered=button_hovered_texture, width=self.window.width / 2, height=150, style=big_button_style)) self.water_simulator_button = self.box.add(arcade.gui.UITextureButton(text="Water Simulator", texture=button_texture, texture_hovered=button_hovered_texture, width=self.window.width / 2, height=self.window.height / 8, style=big_button_style))
self.water_simulator_button.on_click = lambda event: self.water_simulator()
self.physics_playground_button = self.box.add(arcade.gui.UITextureButton(text="Physics Playground", texture=button_texture, texture_hovered=button_hovered_texture, width=self.window.width / 2, height=self.window.height / 8, style=big_button_style))
self.physics_playground_button.on_click = lambda event: self.physics_playground() self.physics_playground_button.on_click = lambda event: self.physics_playground()
self.settings_button = self.box.add(arcade.gui.UITextureButton(text="Settings", texture=button_texture, texture_hovered=button_hovered_texture, width=self.window.width / 2, height=150, style=big_button_style)) self.settings_button = self.box.add(arcade.gui.UITextureButton(text="Settings", texture=button_texture, texture_hovered=button_hovered_texture, width=self.window.width / 2, height=self.window.height / 8, style=big_button_style))
self.settings_button.on_click = lambda event: self.settings() self.settings_button.on_click = lambda event: self.settings()
def physics_playground(self): def physics_playground(self):
from game.physics_playground import Game from game.physics_playground.game import Game
self.window.show_view(Game(self.pypresence_client)) self.window.show_view(Game(self.pypresence_client))
def boid_simulator(self): def boid_simulator(self):
from game.boid_simulator import Game from game.boid_simulator.game import Game
self.window.show_view(Game(self.pypresence_client))
def water_simulator(self):
from game.water_simulator.game import Game
self.window.show_view(Game(self.pypresence_client)) self.window.show_view(Game(self.pypresence_client))
def settings(self): def settings(self):

2
run.py
View File

@@ -25,7 +25,7 @@ timestamp = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
log_filename = f"debug_{timestamp}.log" log_filename = f"debug_{timestamp}.log"
logging.basicConfig(filename=f'{os.path.join(log_dir, log_filename)}', format='%(asctime)s %(name)s %(levelname)s: %(message)s', level=logging.DEBUG) logging.basicConfig(filename=f'{os.path.join(log_dir, log_filename)}', format='%(asctime)s %(name)s %(levelname)s: %(message)s', level=logging.DEBUG)
for logger_name_to_disable in ['arcade', "numba"]: for logger_name_to_disable in ['arcade', "pymunk.shapes", "PIL", "Pillow"]:
logging.getLogger(logger_name_to_disable).propagate = False logging.getLogger(logger_name_to_disable).propagate = False
logging.getLogger(logger_name_to_disable).disabled = True logging.getLogger(logger_name_to_disable).disabled = True

View File

@@ -6,6 +6,9 @@ from arcade.gui.widgets.slider import UISliderStyle
SMALL_RADIUS = 75 SMALL_RADIUS = 75
LARGE_RADIUS = 150 LARGE_RADIUS = 150
WATER_ROWS = 128
WATER_COLS = 128
menu_background_color = (30, 30, 47) menu_background_color = (30, 30, 47)
log_dir = 'logs' log_dir = 'logs'
discord_presence_id = 1414634708414758972 discord_presence_id = 1414634708414758972