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
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):
def __init__(self, pypresence_client):
@@ -11,6 +11,21 @@ class Game(arcade.gui.UIView):
self.boid_sprites = arcade.SpriteList()
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.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("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("Small Radius: {value}", 25, 250, 25, "small_radius", 100)
self.add_setting("Large Radius: {value}", 50, 500, 50, "large_radius", 250)
self.add_setting("Small Radius: {value}", 25, 250, 25, "small_radius")
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):
label = self.settings_box.add(arcade.gui.UILabel(text.format(value=default or 1.0)))
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)))
def save_data(self):
with open("data.json", "w") as file:
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.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):
label.text = text.format(value=value)
self.settings["boid_simulator"][boid_variable] = value
for boid in self.boid_sprites:
setattr(boid, boid_variable, value)
@@ -57,6 +78,8 @@ class Game(arcade.gui.UIView):
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))

View File

@@ -11,7 +11,7 @@ class BodyInventory(arcade.gui.UIGridLayout):
n = 0
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)
n += 1
@@ -22,11 +22,11 @@ class BodyInventory(arcade.gui.UIGridLayout):
self._update_size_hints()
self.items[name] = image
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] = 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].on_click = lambda event, name=name: self.change_to(name)
self.items[name] = image
def change_to(self, name):
self.buttons[self.selected_item] = self.buttons[self.selected_item].with_background(color=arcade.color.TRANSPARENT_BLACK)
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 arcade.gui.experimental.scroll_area import UIScrollArea, UIScrollBar
from pymunk.autogeometry import convex_decomposition
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.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):
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
self.origin_x = 0
self.origin_y = 0
class PhysicsCoin(SpritePhysics):
def __init__(self, pymunk_obj, filename):
super().__init__(pymunk_obj, filename)
@@ -38,10 +47,36 @@ class Game(arcade.gui.UIView):
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.spritelist: arcade.SpriteList[SpritePhysics] = arcade.SpriteList()
self.walls = []
self.custom_bodies = []
self.custom_pymunk_objs = {}
@@ -49,20 +84,24 @@ class Game(arcade.gui.UIView):
self.last_mouse_position = 0, 0
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.gravity_x = 0
self.gravity_y = -900
self.gravity_x = self.settings["physics_playground"].get("gravity_x", 0)
self.gravity_y = self.settings["physics_playground"].get("gravity_y", -930)
self.space.gravity = (self.gravity_x, self.gravity_y)
self.crate_elasticity = 0.5
self.crate_friction = 0.9
self.crate_mass = 1
self.crate_elasticity = self.settings["physics_playground"].get("crate_elasticity", 0.5)
self.crate_friction = self.settings["physics_playground"].get("crate_friction", 0.9)
self.crate_mass = self.settings["physics_playground"].get("crate_mass", 1)
self.coin_elasticity = 0.5
self.coin_friction = 0.9
self.coin_mass = 1
self.coin_elasticity = self.settings["physics_playground"].get("coin_elasticity", 0.5)
self.coin_friction = self.settings["physics_playground"].get("coin_friction", 0.9)
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)))
@@ -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.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_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("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("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("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 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.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()
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):
if gravity_type == "x":
self.gravity_x = value
@@ -117,6 +173,8 @@ class Game(arcade.gui.UIView):
setattr(self, local_variable, value)
self.settings["physics_playground"][local_variable] = value
if pymunk_variable:
for sprite in self.spritelist:
if isinstance(sprite, instance):
@@ -131,7 +189,7 @@ class Game(arcade.gui.UIView):
self.walls.append(pymunk_obj)
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.position = pymunk.Vec2d(x, y)
@@ -146,7 +204,7 @@ class Game(arcade.gui.UIView):
self.spritelist.append(sprite)
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.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)
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):
if button == arcade.MOUSE_BUTTON_LEFT:
self.last_mouse_position = x, y
@@ -219,7 +283,7 @@ class Game(arcade.gui.UIView):
def add_custom_body(self, 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)
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
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)
moment = pymunk.moment_for_poly(1.0, hull)
total_moment = sum(pymunk.moment_for_poly(1.0, part) for part in convex_parts)
png_bytes = cairosvg.svg2png(url=file_path, scale=scale_factor)
image = Image.open(io.BytesIO(png_bytes)).convert("RGBA")
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.clear_custom_body_ui()
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.position = pymunk.Vec2d(x, y)
shape = pymunk.Poly(body, hull)
self.space.add(body, shape)
self.space.add(body)
sprite = SpritePhysics(shape, image)
sprite.origin_x = image.width / 2
sprite.origin_y = image.height / 2
for part in convex_parts:
shape = pymunk.Poly(body, part)
self.space.add(shape)
sprite = CustomPhysics(FakeShape(body), image)
self.spritelist.append(sprite)
@@ -326,13 +394,15 @@ class Game(arcade.gui.UIView):
elif self.inventory_grid.selected_item == "coin":
self.create_coin(self.window.mouse.data['x'], self.window.mouse.data['y'], 10, self.coin_mass)
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:
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:
self.space.remove(sprite.pymunk_obj, sprite.pymunk_obj.body)
body = 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()
self.space.step(self.window._draw_rate)
@@ -342,8 +412,7 @@ class Game(arcade.gui.UIView):
self.dragged_shape.shape.body.velocity = 0, 0
for sprite in self.spritelist:
sprite.center_x = sprite.pymunk_obj.body.position.x + sprite.origin_x
sprite.center_y = sprite.pymunk_obj.body.position.y + sprite.origin_y
sprite.position = sprite.pymunk_obj.body.position
sprite.angle = -math.degrees(sprite.pymunk_obj.body.angle)
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):
if symbol == arcade.key.ESCAPE:
arcade.set_background_color(menu_background_color)
self.save_data()
from menus.main import Main
self.window.show_view(Main(self.pypresence_client))
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)
elif symbol == arcade.key.C:
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()

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.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.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.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()
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))
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))
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"
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).disabled = True

View File

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