Добавил ускоренный 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.

285
common.py
View File

@@ -1,6 +1,7 @@
import os
import json
import uuid
import random
from dataclasses import dataclass, field
from copy import deepcopy
@@ -12,6 +13,14 @@ import pygame_gui
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):
current = data
for key in path:
@@ -146,87 +155,233 @@ def path_exists(data, path):
# print("Путь не найден (нет связи)")
# 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):
"""Находит путь с диагональным движением, но БЕЗ прохода через углы"""
"""★СУПЕРБЫСТРЫЙ BFS: массивы вместо set/deque★"""
rows = len(cells)
if rows == 0:
print("Путь не найден: пустая карта")
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):
if (s_row >= rows or s_col >= cols or
g_row >= rows or g_col >= cols):
print(f"Путь не найден: выход за границы {start} -> {goal}")
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
# ★ МАТРИЦЫ вместо set (10x быстрее хэширования) ★
walkable = [[True] * cols for _ in range(rows)]
rocks_only = [[False] * cols for _ in range(rows)]
start_creature = cells[s_row][s_col].creature_obj
directions = [
(-1, 0), (1, 0), (0, -1), (0, 1), # ортогональ (1.0)
(-1, -1), (-1, 1), (1, -1), (1, 1) # диагональ (1.414)
]
for r in range(rows):
for c in range(cols):
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):
"""Проверяет, можно ли двигаться по диагонали (НЕ через угол)"""
nr, nc = current_r + dr, current_c + dc
# ★ ВЫЧИСЛЯЕМЫЕ МАССИВЫ ★
visited = [[False] * cols for _ in range(rows)]
parent = [[None] * cols for _ in range(rows)]
# ★ БЫСТРАЯ ОЧЕРЕДЬ: индекс вместо deque ★
queue_size = 0
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
directions = [(-1,0), (1,0), (0,-1), (0,1),
(-1,-1), (-1,1), (1,-1), (1,1)]
while front < queue_size:
# ★ БЫСТРОЕ извлечение ★
r, c = queue[front]
front += 1
# Ортогональные соседи ДОЛЖНЫ быть проходимыми для диагонального хода
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):
if r == g_row and c == g_col:
path = []
current = (row, col)
while current in came_from:
path.append(current)
current = came_from[current]
path.append(start)
cr, cc = g_row, g_col
while True:
path.append((cr, cc))
if parent[cr][cc] is None:
break
pr, pc = parent[cr][cc]
cr, cc = pr, pc
return path[::-1]
for dr, dc in directions:
nr, nc = row + dr, col + dc
if 0 <= nr < rows and 0 <= nc < cols and is_passable(cells[nr][nc]):
nr, nc = r + dr, c + dc
if (0 <= nr < rows and 0 <= nc < cols and
walkable[nr][nc] and not visited[nr][nc]):
# ★ ПРОВЕРКА ДИАГОНАЛИ ★
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))
if dr * dc == 0 or can_move_diagonal(r, c, nr, nc, rocks_only, rows, cols):
visited[nr][nc] = True
parent[nr][nc] = (r, c)
# ★ БЫСТРОЕ добавление в очередь ★
queue[queue_size] = [nr, nc]
queue_size += 1
print("Путь не найден")
return None
print(f"Путь не найден: {start} -> {goal}")
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"
},
"item_obj": {},
"creature_obj": {}
"creature_obj": {
"id": "1",
"name": "2",
"sprite_name": "elf_watching"
}
},
{
"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
import eb_objects
import eb_terrain_objects
@@ -68,6 +69,9 @@ class Map:
def move_obj(self, type, start, goal):
"""Перемещает объект типа 'terrain_obj', 'item_obj' или 'creature_obj'
из клетки start=(row, col) в goal=(row, col)"""
if goal is None:
return False
s_y, s_x = start
d_y, d_x = goal
@@ -79,8 +83,9 @@ class Map:
source_cell = self.cells[s_y][s_x]
dest_cell = self.cells[d_y][d_x]
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
setattr(source_cell, type, None)
@@ -287,6 +292,11 @@ class Engine:
gc.collect()
mem_before = process.memory_info().rss / 1024
NUM_ELVES = 1000
elf_count = 0
while running:
time_delta = clock.tick(60)/1000.0
#pygame.event.clear()
@@ -301,6 +311,19 @@ class Engine:
if event.type == pygame.QUIT:
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:
user_text = input_entry.get_text()
exec(user_text)
@@ -349,15 +372,44 @@ class Engine:
if keys[pygame.K_d]:
easy_map.cam_x -= self.camera_step
if keys[pygame.K_q]:
easy_map.scale += self.scale_step
self.spr_scale += self.scale_step
self.scale_sprites()
if keys[pygame.K_e] and easy_map.scale >= self.scale_step:
easy_map.scale -= self.scale_step
self.spr_scale -= self.scale_step
self.scale_sprites()
#if keys[pygame.K_q]:
# easy_map.scale += self.scale_step
# self.spr_scale += self.scale_step
# self.scale_sprites()
#if keys[pygame.K_e] and easy_map.scale >= self.scale_step:
# easy_map.scale -= self.scale_step
# self.spr_scale -= self.scale_step
# 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:
mouse_pos = pygame.mouse.get_pos()
console_rect = console_window.get_abs_rect()

View File

@@ -38,13 +38,28 @@ class Creature(Object):
move_progress: float = 0.0 # 0.0 = старт клетки, 1.0 = конец клетки
current_target: tuple = None # (row, col) следующая клетка
final_goal: tuple = None
move_speed: float = 0.02 # пикселей/кадр (настройте)
render_offset: tuple = (0.0, 0.0)
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):
from common import find_way
self.final_goal = goal
self.start_pos = start
path = find_way(cells, start, goal)
if path and len(path) > 1:
@@ -59,6 +74,24 @@ class Creature(Object):
if self.current_target is None or not self.waypoints:
self.render_offset = (0.0, 0.0)
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 = min(self.move_progress, 1.0)
@@ -66,7 +99,7 @@ class Creature(Object):
if self.move_progress >= 1.0:
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:
self.start_pos = self.current_target # Новая клетка как старт
self.current_target = self.waypoints[0]
@@ -79,7 +112,10 @@ class Creature(Object):
# ★ ТОЛЬКО интерполяция offset ★
start_row, start_col = self.start_pos #or (0, 0)
target_row, target_col = self.current_target
if self.current_target is None:
self.render_offset = (0.0, 0.0)
return
target_row, target_col = self.current_target
offset_x = (target_col - start_col) * cell_size * self.move_progress
offset_y = (target_row - start_row) * cell_size * self.move_progress
self.render_offset = (offset_x, offset_y)

12
main.py
View File

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

View File

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