Добавил ускоренный A* из библиотеки pathfinding - произволительность выросла, но несильно. Пока этот вариант закомментил, сейчас реализация BFS + walkable матрица, работает гораздо лучше, с неоптимальным рендером 100 объектов держит, без рендера 300.

This commit is contained in:
shiva404
2026-02-24 02:03:22 +03:00
parent a707638e09
commit 6b6ca341dc
9 changed files with 337 additions and 81 deletions

Binary file not shown.

273
common.py
View File

@@ -1,6 +1,7 @@
import os import os
import json import json
import uuid import uuid
import random
from dataclasses import dataclass, field from dataclasses import dataclass, field
from copy import deepcopy from copy import deepcopy
@@ -12,6 +13,14 @@ import pygame_gui
from classes import Rock from classes import Rock
from pathfinding.core.grid import Grid
from pathfinding.finder.a_star import AStarFinder
from collections import defaultdict
from heapq import heappush, heappop
from math import sqrt, inf
def path_exists(data, path): def path_exists(data, path):
current = data current = data
for key in path: for key in path:
@@ -146,87 +155,233 @@ def path_exists(data, path):
# print("Путь не найден (нет связи)") # print("Путь не найден (нет связи)")
# return None # return None
#def find_way(cells, start, goal):
# """Находит путь с диагональным движением, но БЕЗ прохода через углы"""
# rows = len(cells)
# if rows == 0:
# return None
# cols = len(cells[0])
#
# def is_passable(cell):
# return (cell.terrain_obj is None or not isinstance(cell.terrain_obj, Rock))
#
# s_row, s_col = start
# g_row, g_col = goal
# if not (0 <= s_row < rows and 0 <= s_col < cols and 0 <= g_row < rows and 0 <= g_col < cols):
# return None
#
# start_cell = cells[s_row][s_col]
# goal_cell = cells[g_row][g_col]
# if not is_passable(start_cell) or not is_passable(goal_cell):
# print(f"Старт/гол непроходимы")
# return None
#
# directions = [
# (-1, 0), (1, 0), (0, -1), (0, 1), # ортогональ (1.0)
# (-1, -1), (-1, 1), (1, -1), (1, 1) # диагональ (1.414)
# ]
#
# def can_move_diagonal(current_r, current_c, dr, dc):
# """Проверяет, можно ли двигаться по диагонали (НЕ через угол)"""
# nr, nc = current_r + dr, current_c + dc
#
# # Ортогональные соседи ДОЛЖНЫ быть проходимыми для диагонального хода
# ortho1_r, ortho1_c = current_r + dr, current_c # вертикальный сосед
# ortho2_r, ortho2_c = current_r, current_c + dc # горизонтальный сосед
#
# # Проверяем границы для ортососедей
# if not (0 <= ortho1_r < rows and 0 <= ortho1_c < cols):
# return False
# if not (0 <= ortho2_r < rows and 0 <= ortho2_c < cols):
# return False
#
# return (is_passable(cells[ortho1_r][ortho1_c]) and
# is_passable(cells[ortho2_r][ortho2_c]))
#
# open_set = []
# h = max(abs(s_row - g_row), abs(s_col - g_col))
# heappush(open_set, (h, 0, s_row, s_col))
#
# came_from = {}
# g_score = defaultdict(lambda: float('inf'))
# g_score[(s_row, s_col)] = 0
#
# while open_set:
# _, _, row, col = heappop(open_set)
# if (row, col) == (g_row, g_col):
# path = []
# current = (row, col)
# while current in came_from:
# path.append(current)
# current = came_from[current]
# path.append(start)
# return path[::-1]
#
# for dr, dc in directions:
# nr, nc = row + dr, col + dc
# if 0 <= nr < rows and 0 <= nc < cols:
# target_cell = cells[nr][nc]
#
# if (nr, nc) != start and target_cell.creature_obj is not None:
# continue
# if not is_passable(target_cell):
# continue
#
# if abs(dr) + abs(dc) == 2:
# if not can_move_diagonal(row, col, dr, dc):
# continue
#
# move_cost = 1.414 if abs(dr) + abs(dc) == 2 else 1.0
# tentative_g = g_score[(row, col)] + move_cost
# pos = (nr, nc)
#
# if tentative_g < g_score[pos]:
# came_from[pos] = (row, col)
# g_score[pos] = tentative_g
# h = max(abs(nr - g_row), abs(nc - g_col))
# f = tentative_g + h
# heappush(open_set, (f, tentative_g, nr, nc))
#
# print("Путь не найден")
# return None
#def find_way(cells, start, goal):
# """A* pathfinding — только новая библиотека"""
# rows = len(cells)
# if rows == 0:
# print("Путь не найден: пустая карта")
# return None
#
# cols = len(cells[0])
#
# # ★ Проверка границ ★
# s_row, s_col = start
# g_row, g_col = goal
# if (s_row >= rows or s_col >= cols or
# g_row >= rows or g_col >= cols):
# print(f"Путь не найден: выход за границы карты {start} -> {goal}")
# return None
#
# # ★ НАХОДИМ существо в start ★
# start_creature = cells[s_row][s_col].creature_obj
#
# # Матрица препятствий
# matrix = [[1 for _ in cells[row]] for row in range(rows)]
#
# for r in range(rows):
# for c in range(cols):
# cell_creature = cells[r][c].creature_obj
# if cell_creature and cell_creature != start_creature:
# matrix[r][c] = 0
#
# from pathfinding.core.grid import Grid
# from pathfinding.finder.a_star import AStarFinder
#
# grid = Grid(matrix=matrix)
# start_node = grid.node(s_row, s_col)
# end_node = grid.node(g_row, g_col)
#
# finder = AStarFinder()
# path_nodes, _ = finder.find_path(start_node, end_node, grid)
#
# if not path_nodes or len(path_nodes) <= 1:
# print(f"Путь не найден: {start} -> {goal}")
# return None
#
# path = [(node.x, node.y) for node in path_nodes]
#
# return path
def find_way(cells, start, goal): def find_way(cells, start, goal):
"""Находит путь с диагональным движением, но БЕЗ прохода через углы""" """★СУПЕРБЫСТРЫЙ BFS: массивы вместо set/deque★"""
rows = len(cells) rows = len(cells)
if rows == 0: if rows == 0:
print("Путь не найден: пустая карта")
return None return None
cols = len(cells[0]) cols = len(cells[0])
def is_passable(cell):
return (cell.terrain_obj is None or not isinstance(cell.terrain_obj, Rock))
s_row, s_col = start s_row, s_col = start
g_row, g_col = goal g_row, g_col = goal
if not (0 <= s_row < rows and 0 <= s_col < cols and 0 <= g_row < rows and 0 <= g_col < cols):
if (s_row >= rows or s_col >= cols or
g_row >= rows or g_col >= cols):
print(f"Путь не найден: выход за границы {start} -> {goal}")
return None return None
start_cell = cells[s_row][s_col] # ★ МАТРИЦЫ вместо set (10x быстрее хэширования) ★
goal_cell = cells[g_row][g_col] walkable = [[True] * cols for _ in range(rows)]
if not is_passable(start_cell) or not is_passable(goal_cell): rocks_only = [[False] * cols for _ in range(rows)]
print(f"Старт/гол непроходимы") start_creature = cells[s_row][s_col].creature_obj
return None
directions = [ for r in range(rows):
(-1, 0), (1, 0), (0, -1), (0, 1), # ортогональ (1.0) for c in range(cols):
(-1, -1), (-1, 1), (1, -1), (1, 1) # диагональ (1.414) cell = cells[r][c]
] if (cell.creature_obj and cell.creature_obj != start_creature) or \
(cell.terrain_obj and cell.terrain_obj.sprite_name == "rock_small"):
walkable[r][c] = False
if cell.terrain_obj and cell.terrain_obj.sprite_name == "rock_small":
rocks_only[r][c] = True
def can_move_diagonal(current_r, current_c, dr, dc): # ★ ВЫЧИСЛЯЕМЫЕ МАССИВЫ ★
"""Проверяет, можно ли двигаться по диагонали (НЕ через угол)""" visited = [[False] * cols for _ in range(rows)]
nr, nc = current_r + dr, current_c + dc parent = [[None] * cols for _ in range(rows)]
# Ортогональные соседи ДОЛЖНЫ быть проходимыми для диагонального хода # ★ БЫСТРАЯ ОЧЕРЕДЬ: индекс вместо deque ★
ortho1_r, ortho1_c = current_r + dr, current_c # вертикальный сосед queue_size = 0
ortho2_r, ortho2_c = current_r, current_c + dc # горизонтальный сосед queue = [[0, 0] for _ in range(rows * cols)] # Предварительно выделяем
queue[0] = [s_row, s_col]
queue_size = 1
front = 0
# Проверяем границы для ортососедей visited[s_row][s_col] = True
if not (0 <= ortho1_r < rows and 0 <= ortho1_c < cols):
return False
if not (0 <= ortho2_r < rows and 0 <= ortho2_c < cols):
return False
return (is_passable(cells[ortho1_r][ortho1_c]) and directions = [(-1,0), (1,0), (0,-1), (0,1),
is_passable(cells[ortho2_r][ortho2_c])) (-1,-1), (-1,1), (1,-1), (1,1)]
open_set = [] while front < queue_size:
h = max(abs(s_row - g_row), abs(s_col - g_col)) # ★ БЫСТРОЕ извлечение ★
heappush(open_set, (h, 0, s_row, s_col)) r, c = queue[front]
front += 1
came_from = {} if r == g_row and c == g_col:
g_score = defaultdict(lambda: float('inf'))
g_score[(s_row, s_col)] = 0
while open_set:
_, _, row, col = heappop(open_set)
if (row, col) == (g_row, g_col):
path = [] path = []
current = (row, col) cr, cc = g_row, g_col
while current in came_from: while True:
path.append(current) path.append((cr, cc))
current = came_from[current] if parent[cr][cc] is None:
path.append(start) break
pr, pc = parent[cr][cc]
cr, cc = pr, pc
return path[::-1] return path[::-1]
for dr, dc in directions: for dr, dc in directions:
nr, nc = row + dr, col + dc nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols and is_passable(cells[nr][nc]):
if (0 <= nr < rows and 0 <= nc < cols and
walkable[nr][nc] and not visited[nr][nc]):
# ★ ПРОВЕРКА ДИАГОНАЛИ ★ # ★ ПРОВЕРКА ДИАГОНАЛИ ★
if abs(dr) + abs(dc) == 2: # диагональное движение if dr * dc == 0 or can_move_diagonal(r, c, nr, nc, rocks_only, rows, cols):
if not can_move_diagonal(row, col, dr, dc): visited[nr][nc] = True
continue # пропускаем запрещённую диагональ parent[nr][nc] = (r, c)
move_cost = 1.414 if abs(dr) + abs(dc) == 2 else 1.0 # ★ БЫСТРОЕ добавление в очередь ★
tentative_g = g_score[(row, col)] + move_cost queue[queue_size] = [nr, nc]
pos = (nr, nc) queue_size += 1
if tentative_g < g_score[pos]: print(f"Путь не найден: {start} -> {goal}")
came_from[pos] = (row, col)
g_score[pos] = tentative_g
h = max(abs(nr - g_row), abs(nc - g_col))
f = tentative_g + h
heappush(open_set, (f, tentative_g, nr, nc))
print("Путь не найден")
return None return None
def can_move_diagonal(r, c, nr, nc, rocks_only, rows, cols):
"""Запрет среза только около rock"""
dr = nr - r
dc = nc - c
r1, c1 = r + dr, c
r2, c2 = r, c + dc
check1_ok = (0 <= r1 < rows and 0 <= c1 < cols and not rocks_only[r1][c1])
check2_ok = (0 <= r2 < rows and 0 <= c2 < cols and not rocks_only[r2][c2])
return check1_ok and check2_ok

View File

@@ -926,7 +926,11 @@
"sprite_name": "grass_small" "sprite_name": "grass_small"
}, },
"item_obj": {}, "item_obj": {},
"creature_obj": {} "creature_obj": {
"id": "1",
"name": "2",
"sprite_name": "elf_watching"
}
}, },
{ {
"terrain_obj": { "terrain_obj": {

View File

@@ -1,4 +1,5 @@
from common import os, json, uuid, deepcopy, dataclass, field from common import os, json, uuid, deepcopy, random
from common import dataclass, field
from common import pygame, pygame_gui from common import pygame, pygame_gui
import eb_objects import eb_objects
import eb_terrain_objects import eb_terrain_objects
@@ -68,6 +69,9 @@ class Map:
def move_obj(self, type, start, goal): def move_obj(self, type, start, goal):
"""Перемещает объект типа 'terrain_obj', 'item_obj' или 'creature_obj' """Перемещает объект типа 'terrain_obj', 'item_obj' или 'creature_obj'
из клетки start=(row, col) в goal=(row, col)""" из клетки start=(row, col) в goal=(row, col)"""
if goal is None:
return False
s_y, s_x = start s_y, s_x = start
d_y, d_x = goal d_y, d_x = goal
@@ -79,8 +83,9 @@ class Map:
source_cell = self.cells[s_y][s_x] source_cell = self.cells[s_y][s_x]
dest_cell = self.cells[d_y][d_x] dest_cell = self.cells[d_y][d_x]
obj = getattr(source_cell, type) obj = getattr(source_cell, type)
check = getattr(dest_cell, type)
if obj is None: if obj is None or check is not None:
return False return False
setattr(source_cell, type, None) setattr(source_cell, type, None)
@@ -287,6 +292,11 @@ class Engine:
gc.collect() gc.collect()
mem_before = process.memory_info().rss / 1024 mem_before = process.memory_info().rss / 1024
NUM_ELVES = 1000
elf_count = 0
while running: while running:
time_delta = clock.tick(60)/1000.0 time_delta = clock.tick(60)/1000.0
#pygame.event.clear() #pygame.event.clear()
@@ -301,6 +311,19 @@ class Engine:
if event.type == pygame.QUIT: if event.type == pygame.QUIT:
running = False running = False
if event.type == pygame.MOUSEWHEEL:
scroll_y = event.y
if scroll_y > 0:
easy_map.scale += self.scale_step * 5
self.spr_scale += self.scale_step * 5
self.scale_sprites()
elif scroll_y < 0 and easy_map.scale >= self.scale_step:
easy_map.scale -= self.scale_step * 5
self.spr_scale -= self.scale_step * 5
self.scale_sprites()
if event.type == pygame_gui.UI_TEXT_ENTRY_FINISHED and event.ui_element == input_entry: if event.type == pygame_gui.UI_TEXT_ENTRY_FINISHED and event.ui_element == input_entry:
user_text = input_entry.get_text() user_text = input_entry.get_text()
exec(user_text) exec(user_text)
@@ -349,14 +372,43 @@ class Engine:
if keys[pygame.K_d]: if keys[pygame.K_d]:
easy_map.cam_x -= self.camera_step easy_map.cam_x -= self.camera_step
if keys[pygame.K_q]: #if keys[pygame.K_q]:
easy_map.scale += self.scale_step # easy_map.scale += self.scale_step
self.spr_scale += self.scale_step # self.spr_scale += self.scale_step
self.scale_sprites() # self.scale_sprites()
if keys[pygame.K_e] and easy_map.scale >= self.scale_step: #if keys[pygame.K_e] and easy_map.scale >= self.scale_step:
easy_map.scale -= self.scale_step # easy_map.scale -= self.scale_step
self.spr_scale -= self.scale_step # self.spr_scale -= self.scale_step
self.scale_sprites() # self.scale_sprites()
if keys[pygame.K_SPACE] and elf_count < NUM_ELVES:
# Находим свободные клетки
# ★ МАССОВЫЙ СПАВН 50 эльфов ★
free_cells = []
for j in range(len(easy_map.cells)): # ★ ЛИМИТ строк ★
for i in range(len(easy_map.cells[j])): # ★ ЛИМИТ колонок ★
cell = easy_map.cells[j][i]
if (cell.creature_obj is None and
cell.terrain_obj and
cell.terrain_obj.sprite_name == "grass_small"):
free_cells.append((j, i))
spawn_count = min(50, len(free_cells)) # ★ Не больше свободных клеток ★
for _ in range(spawn_count):
row, col = random.choice(free_cells)
elf = eb_objects.Creature(id=f"elf_{random.randint(1000,9999)}", name="Elf", sprite_name="elf_watching")
easy_map.cells[row][col].creature_obj = elf
# ★ БЕЗОПАСНЫЙ выбор цели ★
possible_targets = [c for c in free_cells if c != (row, col)]
if possible_targets:
target_cell = random.choice(possible_targets)
elf.move(easy_map.cells, (row, col), target_cell)
free_cells.remove((row, col)) # Убираем занятые клетки
if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1: if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
mouse_pos = pygame.mouse.get_pos() mouse_pos = pygame.mouse.get_pos()

View File

@@ -38,13 +38,28 @@ class Creature(Object):
move_progress: float = 0.0 # 0.0 = старт клетки, 1.0 = конец клетки move_progress: float = 0.0 # 0.0 = старт клетки, 1.0 = конец клетки
current_target: tuple = None # (row, col) следующая клетка current_target: tuple = None # (row, col) следующая клетка
final_goal: tuple = None
move_speed: float = 0.02 # пикселей/кадр (настройте) move_speed: float = 0.02 # пикселей/кадр (настройте)
render_offset: tuple = (0.0, 0.0) render_offset: tuple = (0.0, 0.0)
start_pos: tuple = None # (row, col) начальная позиция сегмента пути start_pos: tuple = None # (row, col) начальная позиция сегмента пути
replan_counter: int = 0
REPLAN_INTERVAL: int = 300
def replan(self, cells, pos):
from common import find_way
path = find_way(cells, pos, self.final_goal)
if path and len(path) > 1:
self.waypoints = path[1:]
self.current_target = self.waypoints[0]
else:
self.waypoints.clear()
self.current_target = None
def move(self, cells, start, goal): def move(self, cells, start, goal):
from common import find_way from common import find_way
self.final_goal = goal
self.start_pos = start self.start_pos = start
path = find_way(cells, start, goal) path = find_way(cells, start, goal)
if path and len(path) > 1: if path and len(path) > 1:
@@ -60,13 +75,31 @@ class Creature(Object):
self.render_offset = (0.0, 0.0) self.render_offset = (0.0, 0.0)
return return
self.replan_counter += 1
if self.replan_counter >= self.REPLAN_INTERVAL:
self.replan_counter = 0
self.replan(map_obj.cells, self.start_pos)
if self.current_target is None: return
target_row, target_col = self.current_target
if (target_row in map_obj.cells and
target_col < len(map_obj.cells[target_row])):
target_cell = map_obj.cells[target_row][target_col]
if target_cell.creature_obj is not None:
self.current_target = None
self.waypoints.clear()
self.render_offset = (0.0, 0.0)
self.replan(map_obj.cells, self.start_pos)
return
self.move_progress += self.move_speed * time_delta * 60 self.move_progress += self.move_speed * time_delta * 60
self.move_progress = min(self.move_progress, 1.0) self.move_progress = min(self.move_progress, 1.0)
if self.move_progress >= 1.0: if self.move_progress >= 1.0:
map_obj.move_obj('creature_obj', self.start_pos, self.current_target) map_obj.move_obj('creature_obj', self.start_pos, self.current_target)
self.waypoints.pop(0) if self.waypoints: self.waypoints.pop(0)
if self.waypoints: if self.waypoints:
self.start_pos = self.current_target # Новая клетка как старт self.start_pos = self.current_target # Новая клетка как старт
self.current_target = self.waypoints[0] self.current_target = self.waypoints[0]
@@ -79,6 +112,9 @@ class Creature(Object):
# ★ ТОЛЬКО интерполяция offset ★ # ★ ТОЛЬКО интерполяция offset ★
start_row, start_col = self.start_pos #or (0, 0) start_row, start_col = self.start_pos #or (0, 0)
if self.current_target is None:
self.render_offset = (0.0, 0.0)
return
target_row, target_col = self.current_target target_row, target_col = self.current_target
offset_x = (target_col - start_col) * cell_size * self.move_progress offset_x = (target_col - start_col) * cell_size * self.move_progress
offset_y = (target_row - start_row) * cell_size * self.move_progress offset_y = (target_row - start_row) * cell_size * self.move_progress

10
main.py
View File

@@ -1,4 +1,6 @@
import eb_engine import eb_engine
#import os
#os.environ['PYTHONJIT'] = '1'
def main(): def main():
e = eb_engine.Engine() e = eb_engine.Engine()
@@ -6,3 +8,11 @@ def main():
if __name__ == "__main__": if __name__ == "__main__":
main() main()
# todo:
# прокрутка баг консоль и карта
# ОПТИМИЗАЦИИ
# очередь задач и задача рандомного патруля
# устроить краш тест поиску пути, запустив много объектов на маленьком поле, успел заметить баги
# добавить функцию движения за каким-то объектом
# сделать, чтобы в случае отменненого движения не телепортировался назад, а плавно
# приступаем к логике

View File

@@ -50,7 +50,6 @@ scale_image() вызывается 150×150=22,500 раз в секунду пр
# проверить у ллм на ошибки - РЕГУЛЯРНАЯ АКТИВНОСТЬ: # проверить у ллм на ошибки - РЕГУЛЯРНАЯ АКТИВНОСТЬ:
# - deepcopy + # - deepcopy +
# - общие + # - общие +
# !!! ДОБАВИТЬ ПРОКРУТКУ И МАСШТАБ КАРТЫ ДЛЯ МЫШИ !!!
# #
# ДОДЕЛАТЬ move для Creature - хранить pos в объекте ??? # ДОДЕЛАТЬ move для Creature - хранить pos в объекте ???
# #