Added camera stabilization, moved the Camera class into its own file, normalized the diagonal camera vector, added a select and multi-select feature, added an inspect feature, changed the world class to use spatial hashing for better storage, changed the world class to use a double buffer to swap states when ticking, added death and interaction flags, added world class object queries, added an abstract base class for a basic entity that should be inherited by all object classes, added a max_visual_width parameter to said base class for selection clarity that should be set by all new entity/object classes, and added a basic interaction framework with an interaction radius parameter. When an object could interact with it's neighbors in the next tick, it should enable its can_interact flag, and the world will provide a list of interactable objects in that radius through the object's tick function. Also removed ruff format precommit check because it currently doesn't work. Also temporarily removed food class because it's outdated.
All checks were successful
Build Simulation and Test / Run All Tests (push) Successful in 2m21s
All checks were successful
Build Simulation and Test / Run All Tests (push) Successful in 2m21s
This commit is contained in:
parent
6952e88e61
commit
b80a5afc4a
@ -20,11 +20,3 @@ repos:
|
|||||||
# Compile requirements
|
# Compile requirements
|
||||||
- id: pip-compile
|
- id: pip-compile
|
||||||
args: [ pyproject.toml, -o, requirements.txt ]
|
args: [ pyproject.toml, -o, requirements.txt ]
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
||||||
# Ruff version.
|
|
||||||
rev: v0.11.12
|
|
||||||
hooks:
|
|
||||||
# Run the linter.
|
|
||||||
- id: ruff
|
|
||||||
# Run the formatter.
|
|
||||||
- id: ruff-format
|
|
||||||
210
main.py
210
main.py
@ -3,7 +3,8 @@ import time
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
from world.world import World, Position
|
from world.world import World, Position
|
||||||
from world.render_objects import DebugRenderObject, FoodObject
|
from world.render_objects import DebugRenderObject
|
||||||
|
from world.simulation_interface import Camera
|
||||||
|
|
||||||
# Initialize Pygame
|
# Initialize Pygame
|
||||||
pygame.init()
|
pygame.init()
|
||||||
@ -16,109 +17,13 @@ DARK_GRAY = (64, 64, 64)
|
|||||||
GRAY = (128, 128, 128)
|
GRAY = (128, 128, 128)
|
||||||
WHITE = (255, 255, 255)
|
WHITE = (255, 255, 255)
|
||||||
RENDER_BUFFER = 50
|
RENDER_BUFFER = 50
|
||||||
SPEED = 700 # Pixels per second
|
|
||||||
|
|
||||||
# Grid settings
|
# Grid settings
|
||||||
GRID_WIDTH = 20 # Number of cells horizontally
|
GRID_WIDTH = 20 # Number of cells horizontally
|
||||||
GRID_HEIGHT = 15 # Number of cells vertically
|
GRID_HEIGHT = 15 # Number of cells vertically
|
||||||
CELL_SIZE = 20 # Size of each cell in pixels
|
CELL_SIZE = 20 # Size of each cell in pixels
|
||||||
|
|
||||||
DEFAULT_TPS = 200 # Amount of ticks per second for the simulation
|
DEFAULT_TPS = 5 # Amount of ticks per second for the simulation
|
||||||
|
|
||||||
|
|
||||||
class Camera:
|
|
||||||
def __init__(self):
|
|
||||||
self.x = 0
|
|
||||||
self.y = 0
|
|
||||||
self.target_x = 0
|
|
||||||
self.target_y = 0
|
|
||||||
self.zoom = 1.0
|
|
||||||
self.target_zoom = 1.0
|
|
||||||
self.smoothing = 0.15 # Higher = more responsive, lower = more smooth
|
|
||||||
self.speed = SPEED
|
|
||||||
self.zoom_smoothing = 0.10
|
|
||||||
self.is_panning = False
|
|
||||||
self.last_mouse_pos = None
|
|
||||||
|
|
||||||
def update(self, keys, deltatime):
|
|
||||||
# Update target position based on input
|
|
||||||
if keys[pygame.K_w]:
|
|
||||||
self.target_y -= self.speed * deltatime / self.zoom
|
|
||||||
if keys[pygame.K_s]:
|
|
||||||
self.target_y += self.speed * deltatime / self.zoom
|
|
||||||
if keys[pygame.K_a]:
|
|
||||||
self.target_x -= self.speed * deltatime / self.zoom
|
|
||||||
if keys[pygame.K_d]:
|
|
||||||
self.target_x += self.speed * deltatime / self.zoom
|
|
||||||
if keys[pygame.K_r]:
|
|
||||||
self.target_x = 0
|
|
||||||
self.target_y = 0
|
|
||||||
|
|
||||||
# Smooth camera movement with drift
|
|
||||||
smoothing_factor = 1 - pow(
|
|
||||||
1 - self.smoothing, deltatime * 60
|
|
||||||
) # Adjust smoothing based on deltatime
|
|
||||||
self.x += (self.target_x - self.x) * smoothing_factor
|
|
||||||
self.y += (self.target_y - self.y) * smoothing_factor
|
|
||||||
|
|
||||||
# Smooth zoom
|
|
||||||
zoom_smoothing_factor = 1 - pow(1 - self.zoom_smoothing, deltatime * 60)
|
|
||||||
self.zoom += (self.target_zoom - self.zoom) * zoom_smoothing_factor
|
|
||||||
|
|
||||||
def handle_zoom(self, zoom_delta):
|
|
||||||
# Zoom in/out with mouse wheel
|
|
||||||
zoom_factor = 1.1
|
|
||||||
if zoom_delta > 0: # Zoom in
|
|
||||||
self.target_zoom *= zoom_factor
|
|
||||||
elif zoom_delta < 0: # Zoom out
|
|
||||||
self.target_zoom /= zoom_factor
|
|
||||||
|
|
||||||
# Clamp zoom levels
|
|
||||||
self.target_zoom = max(0.1, min(5.0, self.target_zoom))
|
|
||||||
|
|
||||||
def start_panning(self, mouse_pos):
|
|
||||||
self.is_panning = True
|
|
||||||
self.last_mouse_pos = mouse_pos
|
|
||||||
|
|
||||||
def stop_panning(self):
|
|
||||||
self.is_panning = False
|
|
||||||
self.last_mouse_pos = None
|
|
||||||
|
|
||||||
def pan(self, mouse_pos):
|
|
||||||
if self.is_panning and self.last_mouse_pos:
|
|
||||||
dx = mouse_pos[0] - self.last_mouse_pos[0]
|
|
||||||
dy = mouse_pos[1] - self.last_mouse_pos[1]
|
|
||||||
self.x -= dx / self.zoom
|
|
||||||
self.y -= dy / self.zoom
|
|
||||||
self.target_x = self.x # Sync target position with actual position
|
|
||||||
self.target_y = self.y
|
|
||||||
self.last_mouse_pos = mouse_pos
|
|
||||||
|
|
||||||
def get_real_coordinates(self, screen_x, screen_y):
|
|
||||||
# Convert screen coordinates to world coordinates
|
|
||||||
world_x = (screen_x - SCREEN_WIDTH // 2 + self.x * self.zoom) / self.zoom
|
|
||||||
world_y = (screen_y - SCREEN_HEIGHT // 2 + self.y * self.zoom) / self.zoom
|
|
||||||
|
|
||||||
return world_x, world_y
|
|
||||||
|
|
||||||
def is_in_view(self, obj_x, obj_y, margin=0):
|
|
||||||
half_w = (SCREEN_WIDTH + (RENDER_BUFFER * self.zoom)) / (2 * self.zoom)
|
|
||||||
half_h = (SCREEN_HEIGHT + (RENDER_BUFFER * self.zoom)) / (2 * self.zoom)
|
|
||||||
cam_left = self.x - half_w
|
|
||||||
cam_right = self.x + half_w
|
|
||||||
cam_top = self.y - half_h
|
|
||||||
cam_bottom = self.y + half_h
|
|
||||||
return (cam_left - margin <= obj_x <= cam_right + margin and
|
|
||||||
cam_top - margin <= obj_y <= cam_bottom + margin)
|
|
||||||
|
|
||||||
def world_to_screen(self, obj_x, obj_y):
|
|
||||||
screen_x = (obj_x - self.x) * self.zoom + SCREEN_WIDTH // 2
|
|
||||||
screen_y = (obj_y - self.y) * self.zoom + SCREEN_HEIGHT // 2
|
|
||||||
return int(screen_x), int(screen_y)
|
|
||||||
|
|
||||||
def get_relative_size(self, world_size):
|
|
||||||
# Converts a world size (e.g., radius or width/height) to screen pixels
|
|
||||||
return int(world_size * self.zoom)
|
|
||||||
|
|
||||||
|
|
||||||
def draw_grid(screen, camera, showing_grid=True):
|
def draw_grid(screen, camera, showing_grid=True):
|
||||||
@ -199,11 +104,12 @@ def draw_grid(screen, camera, showing_grid=True):
|
|||||||
for start, end in horizontal_lines:
|
for start, end in horizontal_lines:
|
||||||
pygame.draw.line(screen, GRAY, start, end)
|
pygame.draw.line(screen, GRAY, start, end)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT), vsync=1)
|
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT), vsync=1)
|
||||||
pygame.display.set_caption("Dynamic Abstraction System Testing")
|
pygame.display.set_caption("Dynamic Abstraction System Testing")
|
||||||
clock = pygame.time.Clock()
|
clock = pygame.time.Clock()
|
||||||
camera = Camera()
|
camera = Camera(SCREEN_WIDTH, SCREEN_HEIGHT, RENDER_BUFFER)
|
||||||
|
|
||||||
is_showing_grid = True # Flag to control grid visibility
|
is_showing_grid = True # Flag to control grid visibility
|
||||||
|
|
||||||
@ -214,6 +120,13 @@ def main():
|
|||||||
last_tps_time = time.perf_counter() # Tracks the last TPS calculation time
|
last_tps_time = time.perf_counter() # Tracks the last TPS calculation time
|
||||||
tick_counter = 0 # Counts ticks executed
|
tick_counter = 0 # Counts ticks executed
|
||||||
actual_tps = 0 # Stores the calculated TPS
|
actual_tps = 0 # Stores the calculated TPS
|
||||||
|
total_ticks = 0 # Total ticks executed
|
||||||
|
|
||||||
|
# Selection state
|
||||||
|
selecting = False
|
||||||
|
select_start = None # (screen_x, screen_y)
|
||||||
|
select_end = None # (screen_x, screen_y)
|
||||||
|
selected_objects = []
|
||||||
|
|
||||||
print("Controls:")
|
print("Controls:")
|
||||||
print("WASD - Move camera")
|
print("WASD - Move camera")
|
||||||
@ -225,8 +138,8 @@ def main():
|
|||||||
# Initialize world
|
# Initialize world
|
||||||
world = World()
|
world = World()
|
||||||
|
|
||||||
world.add_object(DebugRenderObject(Position(0,0)))
|
world.add_object(DebugRenderObject(Position(0, 0)))
|
||||||
world.add_object(FoodObject(Position(100, 0)))
|
world.add_object(DebugRenderObject(Position(20, 0)))
|
||||||
|
|
||||||
running = True
|
running = True
|
||||||
while running:
|
while running:
|
||||||
@ -238,7 +151,8 @@ def main():
|
|||||||
running = False
|
running = False
|
||||||
elif event.type == pygame.KEYDOWN:
|
elif event.type == pygame.KEYDOWN:
|
||||||
if event.key == pygame.K_ESCAPE:
|
if event.key == pygame.K_ESCAPE:
|
||||||
running = False
|
selecting = False
|
||||||
|
selected_objects = []
|
||||||
if event.key == pygame.K_g:
|
if event.key == pygame.K_g:
|
||||||
is_showing_grid = not is_showing_grid
|
is_showing_grid = not is_showing_grid
|
||||||
if event.key == pygame.K_UP:
|
if event.key == pygame.K_UP:
|
||||||
@ -252,11 +166,52 @@ def main():
|
|||||||
elif event.type == pygame.MOUSEBUTTONDOWN:
|
elif event.type == pygame.MOUSEBUTTONDOWN:
|
||||||
if event.button == 2: # Middle mouse button
|
if event.button == 2: # Middle mouse button
|
||||||
camera.start_panning(event.pos)
|
camera.start_panning(event.pos)
|
||||||
|
elif event.button == 1: # Left mouse button
|
||||||
|
selecting = True
|
||||||
|
select_start = event.pos
|
||||||
|
select_end = event.pos
|
||||||
elif event.type == pygame.MOUSEBUTTONUP:
|
elif event.type == pygame.MOUSEBUTTONUP:
|
||||||
if event.button == 2: # Middle mouse button
|
if event.button == 2:
|
||||||
camera.stop_panning()
|
camera.stop_panning()
|
||||||
|
elif event.button == 1 and selecting:
|
||||||
|
selecting = False
|
||||||
|
# Convert screen to world coordinates
|
||||||
|
x1, y1 = camera.get_real_coordinates(*select_start)
|
||||||
|
x2, y2 = camera.get_real_coordinates(*select_end)
|
||||||
|
# If the selection rectangle is very small, treat as a click
|
||||||
|
if (
|
||||||
|
abs(select_start[0] - select_end[0]) < 3
|
||||||
|
and abs(select_start[1] - select_end[1]) < 3
|
||||||
|
):
|
||||||
|
# Single click: select closest object if in range
|
||||||
|
mouse_world_x, mouse_world_y = camera.get_real_coordinates(
|
||||||
|
*select_start
|
||||||
|
)
|
||||||
|
obj = world.query_closest_object(mouse_world_x, mouse_world_y)
|
||||||
|
selected_objects = []
|
||||||
|
if obj:
|
||||||
|
obj_x, obj_y = obj.position.get_position()
|
||||||
|
# Calculate distance in world coordinates
|
||||||
|
dx = obj_x - mouse_world_x
|
||||||
|
dy = obj_y - mouse_world_y
|
||||||
|
dist = (dx ** 2 + dy ** 2) ** 0.5
|
||||||
|
if dist <= obj.max_visual_width / 2:
|
||||||
|
selected_objects = [obj]
|
||||||
|
print(f"Clicked: selected {len(selected_objects)} object(s)")
|
||||||
|
else:
|
||||||
|
# Drag select: select all in rectangle
|
||||||
|
min_x, max_x = min(x1, x2), max(x1, x2)
|
||||||
|
min_y, max_y = min(y1, y2), max(y1, y2)
|
||||||
|
selected_objects = world.query_objects_in_range(
|
||||||
|
min_x, min_y, max_x, max_y
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
f"Selected {len(selected_objects)} objects in range: {min_x}, {min_y} to {max_x}, {max_y}"
|
||||||
|
)
|
||||||
elif event.type == pygame.MOUSEMOTION:
|
elif event.type == pygame.MOUSEMOTION:
|
||||||
camera.pan(event.pos)
|
camera.pan(event.pos)
|
||||||
|
if selecting:
|
||||||
|
select_end = event.pos
|
||||||
|
|
||||||
# Get pressed keys for smooth movement
|
# Get pressed keys for smooth movement
|
||||||
keys = pygame.key.get_pressed()
|
keys = pygame.key.get_pressed()
|
||||||
@ -267,7 +222,9 @@ def main():
|
|||||||
while current_time - last_tick_time >= tick_interval:
|
while current_time - last_tick_time >= tick_interval:
|
||||||
last_tick_time += tick_interval
|
last_tick_time += tick_interval
|
||||||
tick_counter += 1
|
tick_counter += 1
|
||||||
|
total_ticks += 1
|
||||||
# Add your tick-specific logic here
|
# Add your tick-specific logic here
|
||||||
|
|
||||||
print("Tick logic executed")
|
print("Tick logic executed")
|
||||||
world.tick_all()
|
world.tick_all()
|
||||||
|
|
||||||
@ -283,10 +240,37 @@ def main():
|
|||||||
# Render everything in the world
|
# Render everything in the world
|
||||||
world.render_all(camera, screen)
|
world.render_all(camera, screen)
|
||||||
|
|
||||||
|
# Draw selection rectangle if selecting
|
||||||
|
if selecting and select_start and select_end:
|
||||||
|
rect_color = (128, 128, 128, 80) # Gray, semi-transparent
|
||||||
|
border_color = (80, 80, 90) # Slightly darker gray for border
|
||||||
|
|
||||||
|
left = min(select_start[0], select_end[0])
|
||||||
|
top = min(select_start[1], select_end[1])
|
||||||
|
width = abs(select_end[0] - select_start[0])
|
||||||
|
height = abs(select_end[1] - select_start[1])
|
||||||
|
|
||||||
|
s = pygame.Surface((width, height), pygame.SRCALPHA)
|
||||||
|
s.fill(rect_color)
|
||||||
|
screen.blit(s, (left, top))
|
||||||
|
|
||||||
|
# Draw 1-pixel border
|
||||||
|
pygame.draw.rect(
|
||||||
|
screen, border_color, pygame.Rect(left, top, width, height), 1
|
||||||
|
)
|
||||||
|
|
||||||
|
# Draw blue outline for selected objects
|
||||||
|
for obj in selected_objects:
|
||||||
|
obj_x, obj_y = obj.position.get_position()
|
||||||
|
width = obj.max_visual_width if hasattr(obj, "max_visual_width") else 10
|
||||||
|
screen_x, screen_y = camera.world_to_screen(obj_x, obj_y)
|
||||||
|
size = camera.get_relative_size(width)
|
||||||
|
rect = pygame.Rect(screen_x - size // 2, screen_y - size // 2, size, size)
|
||||||
|
pygame.draw.rect(screen, (0, 128, 255), rect, 1) # Blue, 1px wide
|
||||||
|
|
||||||
# Render mouse position as text in top left of screen
|
# Render mouse position as text in top left of screen
|
||||||
mouse_x, mouse_y = camera.get_real_coordinates(*pygame.mouse.get_pos())
|
mouse_x, mouse_y = camera.get_real_coordinates(*pygame.mouse.get_pos())
|
||||||
mouse_text = font.render(f"Mouse: ({mouse_x}, {mouse_y})", True, WHITE)
|
mouse_text = font.render(f"Mouse: ({mouse_x:.2f}, {mouse_y:.2f})", True, WHITE)
|
||||||
text_rect = mouse_text.get_rect()
|
text_rect = mouse_text.get_rect()
|
||||||
text_rect.topleft = (10, 10)
|
text_rect.topleft = (10, 10)
|
||||||
screen.blit(mouse_text, text_rect)
|
screen.blit(mouse_text, text_rect)
|
||||||
@ -303,6 +287,24 @@ def main():
|
|||||||
tps_rect.bottomright = (SCREEN_WIDTH - 10, SCREEN_HEIGHT - 10)
|
tps_rect.bottomright = (SCREEN_WIDTH - 10, SCREEN_HEIGHT - 10)
|
||||||
screen.blit(tps_text, tps_rect)
|
screen.blit(tps_text, tps_rect)
|
||||||
|
|
||||||
|
# Render tick count in bottom left
|
||||||
|
tick_text = font.render(f"Ticks: {total_ticks}", True, WHITE)
|
||||||
|
tick_rect = tick_text.get_rect()
|
||||||
|
tick_rect.bottomleft = (10, SCREEN_HEIGHT - 10)
|
||||||
|
screen.blit(tick_text, tick_rect)
|
||||||
|
|
||||||
|
if len(selected_objects) >= 1:
|
||||||
|
i = 0
|
||||||
|
for each in selected_objects:
|
||||||
|
obj = each
|
||||||
|
obj_text = font.render(
|
||||||
|
f"Object: {str(obj)}, Neighbors: {obj.neighbors}", True, WHITE
|
||||||
|
)
|
||||||
|
obj_rect = obj_text.get_rect()
|
||||||
|
obj_rect.topleft = (10, 30 + i * 20)
|
||||||
|
screen.blit(obj_text, obj_rect)
|
||||||
|
i += 1
|
||||||
|
|
||||||
# Update display
|
# Update display
|
||||||
pygame.display.flip()
|
pygame.display.flip()
|
||||||
clock.tick(180)
|
clock.tick(180)
|
||||||
|
|||||||
@ -1,30 +1,35 @@
|
|||||||
from world.world import Position
|
from world.world import Position, BaseEntity
|
||||||
|
|
||||||
import pygame
|
import pygame
|
||||||
|
|
||||||
class DebugRenderObject:
|
|
||||||
def __init__(self, position: Position):
|
|
||||||
self.position = position
|
|
||||||
|
|
||||||
def tick(self):
|
class DebugRenderObject(BaseEntity):
|
||||||
pass
|
def __init__(self, position: Position, radius=5):
|
||||||
|
super().__init__(position)
|
||||||
|
|
||||||
|
self.neighbors = 0
|
||||||
|
self.radius = radius
|
||||||
|
self.max_visual_width = radius * 2
|
||||||
|
self.interaction_radius = 50
|
||||||
|
self.flags = {
|
||||||
|
"death": False,
|
||||||
|
"can_interact": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
def tick(self, interactable=None):
|
||||||
|
if interactable is None:
|
||||||
|
interactable = []
|
||||||
|
self.neighbors = len(interactable)
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
def render(self, camera, screen):
|
def render(self, camera, screen):
|
||||||
if camera.is_in_view(*self.position.get_position()):
|
if camera.is_in_view(*self.position.get_position()):
|
||||||
pygame.draw.circle(screen, (255,255,255), camera.world_to_screen(*self.position.get_position()), 15 * camera.zoom)
|
pygame.draw.circle(
|
||||||
|
screen,
|
||||||
class FoodObject:
|
(50, 50, min([255, (self.neighbors + 4) * 30])),
|
||||||
def __init__(self, position: Position):
|
camera.world_to_screen(*self.position.get_position()),
|
||||||
self.decay = 0
|
self.radius * camera.zoom,
|
||||||
self.position = position
|
)
|
||||||
|
|
||||||
def tick(self):
|
|
||||||
self.decay += 1
|
|
||||||
|
|
||||||
if (self.decay > 255):
|
|
||||||
self.decay = 0
|
|
||||||
|
|
||||||
def render(self, camera, screen):
|
|
||||||
if camera.is_in_view(*self.position.get_position()):
|
|
||||||
pygame.draw.circle(screen, (255,self.decay,0), camera.world_to_screen(*self.position.get_position()), 5 * camera.zoom)
|
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"DebugRenderObject({self.position}, neighbors={self.neighbors})"
|
||||||
|
|||||||
130
world/simulation_interface.py
Normal file
130
world/simulation_interface.py
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import pygame
|
||||||
|
|
||||||
|
|
||||||
|
class Camera:
|
||||||
|
def __init__(self, screen_width, screen_height, render_buffer=50):
|
||||||
|
self.x = 0
|
||||||
|
self.y = 0
|
||||||
|
self.target_x = 0
|
||||||
|
self.target_y = 0
|
||||||
|
self.zoom = 1.0
|
||||||
|
self.target_zoom = 1.0
|
||||||
|
self.smoothing = 0.15 # Higher = more responsive, lower = more smooth
|
||||||
|
self.speed = 700
|
||||||
|
self.zoom_smoothing = 0.2 # Higher = more responsive, lower = more smooth
|
||||||
|
self.is_panning = False
|
||||||
|
self.last_mouse_pos = None
|
||||||
|
self.screen_width = screen_width
|
||||||
|
self.screen_height = screen_height
|
||||||
|
self.render_buffer = (
|
||||||
|
render_buffer # Buffer for rendering objects outside the screen
|
||||||
|
)
|
||||||
|
|
||||||
|
def update(self, keys, deltatime):
|
||||||
|
# Determine movement direction
|
||||||
|
dx = 0
|
||||||
|
dy = 0
|
||||||
|
if keys[pygame.K_w]:
|
||||||
|
dy -= 1
|
||||||
|
if keys[pygame.K_s]:
|
||||||
|
dy += 1
|
||||||
|
if keys[pygame.K_a]:
|
||||||
|
dx -= 1
|
||||||
|
if keys[pygame.K_d]:
|
||||||
|
dx += 1
|
||||||
|
|
||||||
|
# Normalize direction
|
||||||
|
length = (dx ** 2 + dy ** 2) ** 0.5
|
||||||
|
if length > 0:
|
||||||
|
dx /= length
|
||||||
|
dy /= length
|
||||||
|
|
||||||
|
# Apply movement
|
||||||
|
self.target_x += dx * self.speed * deltatime / self.zoom
|
||||||
|
self.target_y += dy * self.speed * deltatime / self.zoom
|
||||||
|
|
||||||
|
if keys[pygame.K_r]:
|
||||||
|
self.target_x = 0
|
||||||
|
self.target_y = 0
|
||||||
|
|
||||||
|
# Smooth camera movement with drift
|
||||||
|
smoothing_factor = 1 - pow(1 - self.smoothing, deltatime * 60)
|
||||||
|
self.x += (self.target_x - self.x) * smoothing_factor
|
||||||
|
self.y += (self.target_y - self.y) * smoothing_factor
|
||||||
|
|
||||||
|
# Snap to target if within threshold
|
||||||
|
threshold = 0.5
|
||||||
|
if abs(self.x - self.target_x) < threshold:
|
||||||
|
self.x = self.target_x
|
||||||
|
if abs(self.y - self.target_y) < threshold:
|
||||||
|
self.y = self.target_y
|
||||||
|
|
||||||
|
# Smooth zoom
|
||||||
|
zoom_smoothing_factor = 1 - pow(1 - self.zoom_smoothing, deltatime * 60)
|
||||||
|
self.zoom += (self.target_zoom - self.zoom) * zoom_smoothing_factor
|
||||||
|
|
||||||
|
# Snap zoom to target if within threshold
|
||||||
|
zoom_threshold = 0.001
|
||||||
|
if abs(self.zoom - self.target_zoom) < zoom_threshold:
|
||||||
|
self.zoom = self.target_zoom
|
||||||
|
|
||||||
|
def handle_zoom(self, zoom_delta):
|
||||||
|
# Zoom in/out with mouse wheel
|
||||||
|
zoom_factor = 1.1
|
||||||
|
if zoom_delta > 0: # Zoom in
|
||||||
|
self.target_zoom *= zoom_factor
|
||||||
|
elif zoom_delta < 0: # Zoom out
|
||||||
|
self.target_zoom /= zoom_factor
|
||||||
|
|
||||||
|
# Clamp zoom levels
|
||||||
|
self.target_zoom = max(0.1, min(5.0, self.target_zoom))
|
||||||
|
|
||||||
|
def start_panning(self, mouse_pos):
|
||||||
|
self.is_panning = True
|
||||||
|
self.last_mouse_pos = mouse_pos
|
||||||
|
|
||||||
|
def stop_panning(self):
|
||||||
|
self.is_panning = False
|
||||||
|
self.last_mouse_pos = None
|
||||||
|
|
||||||
|
def pan(self, mouse_pos):
|
||||||
|
if self.is_panning and self.last_mouse_pos:
|
||||||
|
dx = mouse_pos[0] - self.last_mouse_pos[0]
|
||||||
|
dy = mouse_pos[1] - self.last_mouse_pos[1]
|
||||||
|
self.x -= dx / self.zoom
|
||||||
|
self.y -= dy / self.zoom
|
||||||
|
self.target_x = self.x # Sync target position with actual position
|
||||||
|
self.target_y = self.y
|
||||||
|
self.last_mouse_pos = mouse_pos
|
||||||
|
|
||||||
|
def get_real_coordinates(self, screen_x, screen_y):
|
||||||
|
# Convert screen coordinates to world coordinates
|
||||||
|
world_x = (screen_x - self.screen_width // 2 + self.x * self.zoom) / self.zoom
|
||||||
|
world_y = (screen_y - self.screen_height // 2 + self.y * self.zoom) / self.zoom
|
||||||
|
|
||||||
|
return world_x, world_y
|
||||||
|
|
||||||
|
def is_in_view(self, obj_x, obj_y, margin=0):
|
||||||
|
half_w = (self.screen_width + (self.render_buffer * self.zoom)) / (
|
||||||
|
2 * self.zoom
|
||||||
|
)
|
||||||
|
half_h = (self.screen_height + (self.render_buffer * self.zoom)) / (
|
||||||
|
2 * self.zoom
|
||||||
|
)
|
||||||
|
cam_left = self.x - half_w
|
||||||
|
cam_right = self.x + half_w
|
||||||
|
cam_top = self.y - half_h
|
||||||
|
cam_bottom = self.y + half_h
|
||||||
|
return (
|
||||||
|
cam_left - margin <= obj_x <= cam_right + margin
|
||||||
|
and cam_top - margin <= obj_y <= cam_bottom + margin
|
||||||
|
)
|
||||||
|
|
||||||
|
def world_to_screen(self, obj_x, obj_y):
|
||||||
|
screen_x = (obj_x - self.x) * self.zoom + self.screen_width // 2
|
||||||
|
screen_y = (obj_y - self.y) * self.zoom + self.screen_height // 2
|
||||||
|
return int(screen_x), int(screen_y)
|
||||||
|
|
||||||
|
def get_relative_size(self, world_size):
|
||||||
|
# Converts a world size (e.g., radius or width/height) to screen pixels
|
||||||
|
return int(world_size * self.zoom)
|
||||||
128
world/world.py
128
world/world.py
@ -1,18 +1,129 @@
|
|||||||
|
from collections import defaultdict
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
|
||||||
class World:
|
class World:
|
||||||
def __init__(self):
|
def __init__(self, partition_size=10):
|
||||||
self.objects = []
|
self.partition_size = partition_size
|
||||||
pass
|
self.buffers = [defaultdict(list), defaultdict(list)]
|
||||||
|
self.current_buffer = 0
|
||||||
|
|
||||||
|
def _hash_position(self, position):
|
||||||
|
# Map world coordinates to cell coordinates
|
||||||
|
return int(position.x // self.partition_size), int(
|
||||||
|
position.y // self.partition_size
|
||||||
|
)
|
||||||
|
|
||||||
def render_all(self, camera, screen):
|
def render_all(self, camera, screen):
|
||||||
for obj in self.objects:
|
for obj_list in self.buffers[self.current_buffer].values():
|
||||||
obj.render(camera, screen)
|
for obj in obj_list:
|
||||||
|
obj.render(camera, screen)
|
||||||
|
|
||||||
def tick_all(self):
|
def tick_all(self):
|
||||||
for obj in self.objects:
|
next_buffer = 1 - self.current_buffer
|
||||||
obj.tick()
|
self.buffers[next_buffer].clear()
|
||||||
|
|
||||||
|
# print all objects in the current buffer
|
||||||
|
print(
|
||||||
|
f"Ticking objects in buffer {self.current_buffer}:",
|
||||||
|
self.buffers[self.current_buffer].values(),
|
||||||
|
)
|
||||||
|
|
||||||
|
for obj_list in self.buffers[self.current_buffer].values():
|
||||||
|
for obj in obj_list:
|
||||||
|
if obj.flags["death"]:
|
||||||
|
continue
|
||||||
|
if obj.flags["can_interact"]:
|
||||||
|
interactable = self.query_objects_within_radius(
|
||||||
|
obj.position.x, obj.position.y, obj.interaction_radius
|
||||||
|
)
|
||||||
|
interactable.remove(obj)
|
||||||
|
print(f"Object {obj} interacting with {len(interactable)} objects.")
|
||||||
|
new_obj = obj.tick(interactable)
|
||||||
|
else:
|
||||||
|
new_obj = obj.tick()
|
||||||
|
if new_obj is None:
|
||||||
|
continue
|
||||||
|
cell = self._hash_position(new_obj.position)
|
||||||
|
self.buffers[next_buffer][cell].append(new_obj)
|
||||||
|
self.current_buffer = next_buffer
|
||||||
|
|
||||||
def add_object(self, new_object):
|
def add_object(self, new_object):
|
||||||
self.objects.append(new_object)
|
cell = self._hash_position(new_object.position)
|
||||||
|
self.buffers[self.current_buffer][cell].append(new_object)
|
||||||
|
|
||||||
|
def query_objects_within_radius(self, x, y, radius):
|
||||||
|
result = []
|
||||||
|
cell_x, cell_y = int(x // self.partition_size), int(y // self.partition_size)
|
||||||
|
cells_to_check = []
|
||||||
|
r = int((radius // self.partition_size) + 1)
|
||||||
|
for dx in range(-r, r + 1):
|
||||||
|
for dy in range(-r, r + 1):
|
||||||
|
cells_to_check.append((cell_x + dx, cell_y + dy))
|
||||||
|
for cell in cells_to_check:
|
||||||
|
for obj in self.buffers[self.current_buffer].get(cell, []):
|
||||||
|
obj_x, obj_y = obj.position.get_position()
|
||||||
|
dx = obj_x - x
|
||||||
|
dy = obj_y - y
|
||||||
|
if dx * dx + dy * dy <= radius * radius:
|
||||||
|
result.append(obj)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def query_objects_in_range(self, x1, y1, x2, y2):
|
||||||
|
result = []
|
||||||
|
cell_x1, cell_y1 = (
|
||||||
|
int(x1 // self.partition_size),
|
||||||
|
int(y1 // self.partition_size),
|
||||||
|
)
|
||||||
|
cell_x2, cell_y2 = (
|
||||||
|
int(x2 // self.partition_size),
|
||||||
|
int(y2 // self.partition_size),
|
||||||
|
)
|
||||||
|
for cell_x in range(cell_x1, cell_x2 + 1):
|
||||||
|
for cell_y in range(cell_y1, cell_y2 + 1):
|
||||||
|
for obj in self.buffers[self.current_buffer].get((cell_x, cell_y), []):
|
||||||
|
obj_x, obj_y = obj.position.get_position()
|
||||||
|
if x1 <= obj_x <= x2 and y1 <= obj_y <= y2:
|
||||||
|
result.append(obj)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def query_closest_object(self, x, y):
|
||||||
|
closest_obj = None
|
||||||
|
closest_distance = float("inf")
|
||||||
|
for obj_list in self.buffers[self.current_buffer].values():
|
||||||
|
for obj in obj_list:
|
||||||
|
obj_x, obj_y = obj.position.get_position()
|
||||||
|
dx = obj_x - x
|
||||||
|
dy = obj_y - y
|
||||||
|
distance = dx * dx + dy * dy
|
||||||
|
if distance < closest_distance:
|
||||||
|
closest_distance = distance
|
||||||
|
closest_obj = obj
|
||||||
|
return closest_obj
|
||||||
|
|
||||||
|
|
||||||
|
class BaseEntity(ABC):
|
||||||
|
def __init__(self, position: "Position"):
|
||||||
|
self.position = position
|
||||||
|
self.interaction_radius = 0
|
||||||
|
self.flags = {
|
||||||
|
"death": False,
|
||||||
|
"can_interact": False,
|
||||||
|
}
|
||||||
|
self.world_callbacks = {}
|
||||||
|
self.max_visual_width = 0
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def tick(self, interactable=None):
|
||||||
|
return self
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def render(self, camera, screen):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def flag_for_death(self):
|
||||||
|
self.flags["death"] = True
|
||||||
|
|
||||||
|
|
||||||
class Position:
|
class Position:
|
||||||
def __init__(self, x, y):
|
def __init__(self, x, y):
|
||||||
@ -31,4 +142,3 @@ class Position:
|
|||||||
|
|
||||||
def get_position(self):
|
def get_position(self):
|
||||||
return self.x, self.y
|
return self.x, self.y
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user