Привет, Хабр! Сегодня мы поговорим о том, как сделать код не просто красивым, но и живым. Звучит как научная фантастика, либо вы уже подготовились к очередной банальности про искусственный интеллект, но не в этом посте. В 1970 году британский математик Джон Хортон Конвей показал миру, что даже простейшие алгоритмы могут порождать сложные, живые системы, которые ещё и к тому же полные по Тьюрингу. И что код может быть не только красивым, но и живым.
К слову о красивом коде, слышали про «Конкурс красоты кода» от Сбера? Вот где можно развернуться по полной. Выбирайте из имеющихся заданий которое понравится и творите что вашей душе угодно. Главное — это ваши идеи и умение их воплотить в коде. Так что не стесняйтесь, регистрируйтесь на конкурс и покажите, как должен выглядеть по‑настоящему красивый код. А там, глядишь, и призы какие‑нибудь получите. Ну что, готовы принять вызов? Тогда вперёд, за красивым кодом!
А теперь вернёмся к «Живому коду». Конвей придумал «Игру жизни» — клеточный автомат, который при всей своей простоте способен генерировать невероятно сложные паттерны, имитирующие жизнь. И сегодня мы воссоздадим эту игру, используя современные инструменты. Но чтобы сделать это чуть менее банально, чем обычно — мы сделаем это на open-source движке Godot, который в последнее время набирает популярность как достойный конкурент Unity, особенно в мире инди‑разработки.
Почему Godot? Во‑первых, как уже сказали, он опенсорсный, что уже даёт +10 очков относительно проприетарного собрата, требующего лицензионных отчислений. А также потому что это как швейцарский нож в мире игровых движков: вроде и функций куча, а весит всего ничего, менее гигабайта. И веб‑экспорт у него, по идее, столь же прост и однокнопочен, как в Unity, что мы и проверим. А мы как раз хотим не просто сделать игру, но и сделать её пригодной для размещения на каком‑нибудь хостинге в качестве демки, чтобы каждый мог посмотреть на красоту живого кода в действии отрендеренной HTML‑страницы.
Подготовка: структура проекта
Для начала давайте разберёмся, что нам понадобится для создания нашей «Игры жизни». Открываем Godot и создаём новый проект. Вот что нам нужно сделать:
-
Создать две сцены:
-
game.tscn (основная сцена игры);
-
cell.tscn (сцена для отдельной клетки);
-
-
В game.tscn добавить следующие ноды:
-
Game (Node2D) (корневая нода);
-
CheckButton (для запуска и остановки симуляции);
-
InteractiveButton (тоже CheckButton но переименованная, для переключения интерактивного режима);
-
-
В cell.tscn добавить только спрайт с текстурой для отображения живой клетки.
-
В Editor → Manage Export Templates заранее нажмём «Download and Install» для шаблонов экспорта, что пригодится нам ближе к финалу.
Как можно заметить, это и близко не ракетостроение и не сложнее создания проекта что в Unity, что в Unreal.
Фундамент: базовая функциональность
Основной скрипт
Начнём с написания скрипта для основной сцены. Создаём файл game.gd и прикрепляем его к ноде Game. Вот базовая структура:
extends Node2D @export var cell_scene : PackedScene var row_count : int = 45 var column_count : int = 80 var cell_width: int = 15 var cell_matrix: Array = [] var previous_cell_states: Array = [] var is_game_running: bool = false var is_interactive_mode: bool = false
Что здесь происходит? Мы определяем основные переменные для нашей игры. @export var cell_scene : PackedScene
— это особая фишка Godot, которая позволяет нам через инспектор связать сцену клетки с основной сценой. Весьма удобно, пусть и ничего необычного.
Но одних переменных мало. Нам нужно как-то инициализировать наше игровое поле. Добавляем функцию:
func initialize_game(): cell_matrix.clear() previous_cell_states.clear() for column in range(column_count): cell_matrix.push_back([]) previous_cell_states.push_back([]) for row in range(row_count): var cell = cell_scene.instantiate() self.add_child(cell) cell.position = Vector2(column * cell_width, row * cell_width) cell.visible = false previous_cell_states[column].push_back(false) cell_matrix[column].push_back(cell)
Она создаёт двумерный массив клеток, аккуратно располагая их на игровом поле. Каждая клетка изначально невидима (считай, мертва). Но это пока.
Правила игры
Теперь самое интересное — реализация правил «Игры жизни». Добавляем функции для подсчёта живых соседей и определения следующего состояния клетки:
func get_count_of_alive_neighbours(column, row): var count = 0 for x in range(-1, 2): for y in range(-1, 2): if not (x == 0 and y == 0): var neighbor_column = column + x var neighbor_row = row + y if neighbor_column >= 0 and neighbor_column < column_count and neighbor_row >= 0 and neighbor_row < row_count: if previous_cell_states[neighbor_column][neighbor_row]: count += 1 return count func get_next_state(column, row): var current = previous_cell_states[column][row] var neighbours_alive = get_count_of_alive_neighbours(column, row) if current: return neighbours_alive == 2 or neighbours_alive == 3 else: return neighbours_alive == 3
Эти функции — сердце нашей игры. Они реализуют классические правила Конвея: клетка оживает, если у неё ровно три живых соседа, и выживает, если у неё два или три соседа. Просто? Да. Эффективно? Безусловно!
Жизненный цикл: обновление состояния игры
Теперь нам нужна функция, которая будет обновлять состояние всего игрового поля. Добавляем:
func update_game_state(): for column in range(column_count): for row in range(row_count): previous_cell_states[column][row] = cell_matrix[column][row].visible for column in range(column_count): for row in range(row_count): cell_matrix[column][row].visible = get_next_state(column, row)
Эта функция — дирижёр нашего клеточного оркестра. Сначала она сохраняет текущее состояние всех клеток, а затем обновляет их на основе правил игры.
Интерактивность: пусть игрок тоже поучаствует
Но что за игра без игрока? Добавим обработку пользовательского ввода, чтобы наш виртуальный биолог мог взаимодействовать с клетками:
func _input(event): if event is InputEventMouseButton: if event.button_index == MOUSE_BUTTON_LEFT and event.pressed: var click_position = get_global_mouse_position() var column = int(click_position.x / cell_width) var row = int(click_position.y / cell_width) if column >= 0 and column < column_count and row >= 0 and row < row_count: toggle_cell(column, row) func toggle_cell(column, row): if not is_game_running or is_interactive_mode: cell_matrix[column][row].visible = not cell_matrix[column][row].visible previous_cell_states[column][row] = cell_matrix[column][row].visible
Эти функции позволяют игроку щёлкать по клеткам, чтобы переключать их состояние. Теперь наша виртуальная экосистема не только живая, но и интерактивная.
Управление: кнопки — наше всё
Помните, мы добавили в сцену две кнопки? Пора заставить их работать. Создадим для них отдельные скрипты.
check_button.gd:
extends CheckButton signal game_state_changed(is_running: bool) func _ready(): text = "Стоп" toggled.connect(_on_toggled) func _on_toggled(button_pressed: bool): text = "Стоп" if button_pressed else "Старт" emit_signal("game_state_changed", button_pressed)
Interactive_button.gd:
extends CheckButton signal interactive_mode_changed(is_interactive: bool) func _ready(): text = "Интерактив: Выкл" toggled.connect(_on_toggled) func _on_toggled(button_pressed: bool): text = "Интерактив: Вкл" if button_pressed else "Интерактив: Выкл" emit_signal("interactive_mode_changed", button_pressed)
Связываем всё воедино
Теперь нам нужно связать все эти части вместе. Дополняем наш основной скрипт:
game.gd
var row_count : int = 45 var column_count : int = 80 var cell_width: int = 15 var cell_matrix: Array = [] var previous_cell_states: Array = [] var is_game_running: bool = false var is_interactive_mode: bool = false var is_mouse_pressed: bool = false var last_toggled_cell: Vector2 = Vector2(-1, -1) @onready var check_button = $CheckButton @onready var interactive_button = $InteractiveButton @onready var update_timer = $UpdateTimer # Константы для позиционирования и размера кнопок const BUTTON_MARGIN: int = 10 const BUTTON_WIDTH: int = 120 const BUTTON_HEIGHT: int = 40 const UPDATE_INTERVAL: float = 0.5 # Полсекунды # Цвет линий сетки и цвет фона const GRID_COLOR: Color = Color.BLACK const BACKGROUND_COLOR: Color = Color.WEB_GRAY # Отдельный узел для сетки var grid_node: Node2D func _ready(): # Подключаем сигнал изменения размера окна get_tree().root.size_changed.connect(self.on_window_resize) # Подключаем сигналы изменения состояния игры и интерактивного режима check_button.game_state_changed.connect(_on_game_state_changed) interactive_button.interactive_mode_changed.connect(_on_interactive_mode_changed) # Устанавливаем свойства кнопок check_button.size = Vector2(BUTTON_WIDTH, BUTTON_HEIGHT) check_button.text = "Старт" interactive_button.size = Vector2(BUTTON_WIDTH, BUTTON_HEIGHT) interactive_button.text = "Интерактивный режим" # Настраиваем таймер update_timer = Timer.new() add_child(update_timer) update_timer.connect("timeout", Callable(self, "_on_update_timer_timeout")) update_timer.set_wait_time(UPDATE_INTERVAL) update_timer.set_one_shot(false) # Создаем узел сетки grid_node = Node2D.new() add_child(grid_node) on_window_resize() func draw_grid(): # Удаляем старый узел сетки и создаем новый grid_node.queue_free() grid_node = Node2D.new() add_child(grid_node) grid_node.draw.connect(self._on_grid_draw) grid_node.queue_redraw() func _on_grid_draw(): # Рисуем белый фон var background_rect = Rect2( 0, BUTTON_HEIGHT + BUTTON_MARGIN, column_count * cell_width, row_count * cell_width ) grid_node.draw_rect(background_rect, BACKGROUND_COLOR) # Рисуем вертикальные линии сетки for x in range(column_count + 1): var start = Vector2(x * cell_width, BUTTON_HEIGHT + BUTTON_MARGIN) var end = Vector2(x * cell_width, BUTTON_HEIGHT + BUTTON_MARGIN + row_count * cell_width) grid_node.draw_line(start, end, GRID_COLOR) # Рисуем горизонтальные линии сетки for y in range(row_count + 1): var start = Vector2(0, BUTTON_HEIGHT + BUTTON_MARGIN + y * cell_width) var end = Vector2(column_count * cell_width, BUTTON_HEIGHT + BUTTON_MARGIN + y * cell_width) grid_node.draw_line(start, end, GRID_COLOR) func _on_game_state_changed(is_running: bool): is_game_running = is_running if is_game_running: update_timer.start() else: update_timer.stop() func _on_interactive_mode_changed(is_interactive: bool): is_interactive_mode = is_interactive func _on_update_timer_timeout(): update_game_state() func initialize_game(): # Очищаем матрицы и удаляем старые ячейки cell_matrix.clear() previous_cell_states.clear() for child in get_children(): if child != check_button and child != interactive_button and child != update_timer and child != grid_node: child.queue_free() # Рисуем сетку перед созданием ячеек draw_grid() # Создаем новые ячейки for column in range(column_count): cell_matrix.push_back([]) previous_cell_states.push_back([]) for row in range(row_count): var cell = cell_scene.instantiate() self.add_child(cell) cell.position = Vector2(column * cell_width, row * cell_width + BUTTON_HEIGHT + BUTTON_MARGIN) cell.visible = false previous_cell_states[column].push_back(false) cell_matrix[column].push_back(cell) # Включаем обработку ввода set_process_input(true) func on_window_resize(): # Пересчитываем размеры игрового поля при изменении размера окна var window_size = get_viewport_rect().size column_count = int(window_size.x / cell_width) row_count = int((window_size.y - BUTTON_HEIGHT - BUTTON_MARGIN) / cell_width) # Позиционируем кнопки check_button.position = Vector2(BUTTON_MARGIN, BUTTON_MARGIN) interactive_button.position = Vector2(BUTTON_MARGIN * 2 + BUTTON_WIDTH, BUTTON_MARGIN) initialize_game() func is_edge(column, row): # Проверяем, является ли ячейка краевой return row == 0 or column == 0 or row == row_count-1 or column == column_count-1 func get_count_of_alive_neighbours(column, row): # Подсчитываем количество живых соседей для заданной ячейки var count = 0 for x in range(-1, 2): for y in range(-1, 2): if not (x == 0 and y == 0): var neighbor_column = column + x var neighbor_row = row + y if neighbor_column >= 0 and neighbor_column < column_count and neighbor_row >= 0 and neighbor_row < row_count: if previous_cell_states[neighbor_column][neighbor_row]: count += 1 return count func get_next_state(column, row): # Определяем следующее состояние ячейки согласно правилам игры if is_edge(column, row): return false var current = previous_cell_states[column][row] var neighbours_alive = get_count_of_alive_neighbours(column, row) if current: # Ячейка жива return neighbours_alive == 2 or neighbours_alive == 3 else: # Ячейка мертва return neighbours_alive == 3 func update_game_state(): # Сохраняем текущее состояние ячеек for column in range(column_count): for row in range(row_count): previous_cell_states[column][row] = cell_matrix[column][row].visible # Обновляем состояние ячеек for column in range(column_count): for row in range(row_count): cell_matrix[column][row].visible = get_next_state(column, row) func _input(event): # Обрабатываем пользовательский ввод if event is InputEventMouseButton: if event.button_index == MOUSE_BUTTON_LEFT: is_mouse_pressed = event.pressed if is_mouse_pressed: toggle_cell_at_mouse_position() elif event is InputEventMouseMotion and is_mouse_pressed: toggle_cell_at_mouse_position() func toggle_cell_at_mouse_position(): # Переключаем состояние ячейки под курсором мыши var click_position = get_global_mouse_position() var column = int((click_position.x) / cell_width) var row = int((click_position.y - BUTTON_HEIGHT - BUTTON_MARGIN) / cell_width) if column >= 0 and column < column_count and row >= 0 and row < row_count: var current_cell = Vector2(column, row) if current_cell != last_toggled_cell: toggle_cell(column, row) last_toggled_cell = current_cell func toggle_cell(column, row): # Переключаем состояние конкретной ячейки if not is_game_running or is_interactive_mode: cell_matrix[column][row].visible = not cell_matrix[column][row].visible previous_cell_states[column][row] = cell_matrix[column][row].visible
Теперь наша игра запускается, останавливается, позволяет взаимодействовать с клетками — всё начинает оживать.
Финальный штрих: экспорт в HTML5
Теперь, когда наша «Игра жизни» готова, давайте сделаем её живой и в браузере. Godot делает экспорт в HTML5 через ряд достаточно простых действий:
-
переходим в меню «Project» → «Export»;
-
нажимаем «Add» и выбираем «Web»;
-
настраиваем параметры, либо ничего не трогаем;
-
жмём «Export All».
Получившийся файл мы дальше переименовываем в index.html и нет, не запускаем, для этого нам потребуется HTTPS‑сервер. Запустим его с помощью следующего кода на Python (server.py), в той же папке, что и наш index.html:
import http.server, ssl, os # Абсолютный путь до сервера thisScriptPath = os.path.dirname(os.path.abspath(__file__)) + '/' # Создаём самоподписанный сертификат через openssl def generate_selfsigned_cert(): try: OpenSslCommand = 'openssl req -newkey rsa:4096 -x509 -sha256 -days 3650 -nodes -out ' + thisScriptPath + 'cert.pem -keyout ' + thisScriptPath + 'key.pem -subj "/C=IN/ST=Maharashtra/L=Satara/O=Wannabees/OU=KahiHiHa Department/CN=www.iamselfdepartment.com"' os.system(OpenSslCommand) print('<<<<Certificate Generated>>>>>>') except Exception as e: print(f'Error while generating certificate: {e}') # Запускаем сервер на заданном порту def startServer(host, port): server_address = (host, port) httpd = http.server.HTTPServer(server_address, http.server.SimpleHTTPRequestHandler) # Создаём SSL-сертификат context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) context.load_cert_chain(certfile=thisScriptPath + "cert.pem", keyfile=thisScriptPath + "key.pem") # Оборачиваем сокет в SSL httpd.socket = context.wrap_socket(httpd.socket, server_side=True) print("File Server started at https://" + server_address[0] + ":" + str(server_address[1])) httpd.serve_forever() # запускаем скрипт def main(): try: generate_selfsigned_cert() # адрес и порт можно поменять startServer('localhost', 8000) except KeyboardInterrupt: print("\nFile Server Stopped!") except Exception as e: print(f"Error starting server: {e}") # вызываем основную функцию main()
Переходим далее по созданному адресу, и вуаля — игра готова!
Ну что, мы прошли путь от простых правил до живого организма на экране. Красота, не правда ли? И ведь это только верхушка айсберга того, на что способен по‑настоящему красивый код.
ссылка на оригинал статьи https://habr.com/ru/articles/852220/
Добавить комментарий