mirror of
https://github.com/csd4ni3l/simulator-games.git
synced 2025-11-05 03:58:16 +01:00
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:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -180,3 +180,4 @@ test*.py
|
||||
logs/
|
||||
logs
|
||||
settings.json
|
||||
data.json
|
||||
@@ -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 |
@@ -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))
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
146
game/water_simulator/game.py
Normal file
146
game/water_simulator/game.py
Normal 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()
|
||||
80
game/water_simulator/shader.py
Normal file
80
game/water_simulator/shader.py
Normal 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
|
||||
@@ -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
2
run.py
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user