Прежде чем приступать к обзору (пусть и краткому) возможных архитектур и алгоритмов для игры, необходимо обговорить некоторые особенности работы игр.
Любая игра - это обычная программа, которая запускается на вашем компьютере и работает по тем же правилам, что и все остальные программы, построена на тех же принципах. Она может представлять из себя один большой exe-файл, запускающий один единственный процесс с одним единственным потоком. Или быть набором сотен файлов с ресурсами игры (текстурами, библиотеками и прочим), работающая в виде нескольких независимых процессов (“программ”), каждый из которых содержит в себе несколько параллельных потоков выполнения команд с множеством объектов, систем (скажем, набором объектов/сущностей) и сложными взаимосвязями между ними.
Несмотря на разнообразие подходов к разработке игры, можно выделить несколько распространенных идей, лежащих в основе любой игры.
Основной цикл игры включает в себя обработку ввода пользователя, выполнение соответствующих действий, воздействие на игровой мир и его отрисовку.
Пример последовательности действий в игровом цикле:
Причем в теории, какие-то вещи мы можем делать параллельно, но лучше разобраться с простыми, однопоточными вариантами. Так что эти действия хотелось бы выполнить в четком порядке, иначе все сломается. Если мы отрисуем сначала картинку, а потом передвинем персонажа, то для пользователя это будет выглядеть так, словно ничего не произошло.
Обработка ввода и других действий в игре может казаться мгновенной, но на самом деле требует времени. Компьютеры обрабатывают команды очень быстро, что позволяет играм создавать впечатление работы в реальном времени, а не в пошаговом режиме. Но, конечно, это команды все так же выполняются одна за другой.
Необходимо помнить о том, что ваш код компилируется в промежуточное представление (байткод) - “ассемблер” виртуальной машины C#, т.е. набор команд для процессора, а затем выполняется на этой виртуальной машине (ВМ). Что такое виртуальная машина в данном случае? Это обычная программа, которая выполняет роль некоторого универсального компьютера, что позволяет C# работать на большом количестве разных платформ. Так вот, ВМ компилирует при запуске ваш байткод в ассемблер физического процессора, на котором запускается данная ВМ. И вот процессор выполняет этот набор примитивных команд последовательно (не рассматриваем параллельность). Но делает это с огромной скоростью. Условно скажем - миллиарды команд в секунду.
Для Python идея такая же в общих чертах.
Соответственно, программы, и игры в том числе, работают с некоторыми задержками, которые не заметны человеческому глазу. На самом деле, задержек гораздо больше - взять, к примеру, задержку изменения цвета пикселя на мониторе. Если она слишком большая, пользователь будет считать, что изображение двоится/троится/размывается.
Но если все работает пошагово, как делать анимации? Графику? ИИ? Давайте для начала упростим вопрос.
Как нам считать условную скорость персонажа? В пикселях в секунду? В командах процессора в секунду? Не совсем. Можно ввести дополнительные временные шкалы, которые частично будут "привязаны" к CPU.
Нам не нужно обновлять каждое мгновение абсолютно все данные. Если никакие объекты не сдвинулись, то зачем проверять столкновения объектов? Если мы работаем с сетью, то мы не можем каждое мгновение отправлять и получать данные. Сигналу надо пройти тысячи километров, потратить на это время, поэтому мы получаем и отправляем данные, условно, только раз в секунду. И т.д. Поэтому некоторые системы, которые будут в вашей игре будут выполнять обновление каждую итерацию игрового цикла, другие - каждые три итерации и т.д.
Иногда нам необходимо приостановить игру. Но это не означает приостановить всю программу, подразумевается приостановка игрового процесса: монстры не атакуют, внутриигровое время не идет, персонаж не двигается, но вы можете нажимать на кнопки меню, к примеру.
Мы хотели бы измерять, к примеру, скорость персонажа в метрах в секунду. Но эта секунда не должна зависеть от того, сколько кадров мы нарисовали, сколько времени реального времени прошло. Или наоборот, внутриигровая секунда должна совпадать с реальной секундой, вне зависимости от того, мощный или слабый у вас компьютер.
Использование шкалы B для всех игровых расчетов (скорости персонажа, времени и т.д.) и шкалы A для программных расчетов позволяет гибко управлять временем в игре, включая возможность его замедления, ускорения или полной остановки.
Как-то так:
import pygame
import sys
class Game:
def __init__(self):
# Settings
self.is_paused = False
self.game_speed = 1.0
self.delta_time = 0.016 # 60 FPS (16 ms)
def toggle_pause(self):
self.is_paused = not self.is_paused
def set_game_speed(self, speed):
self.game_speed = speed
def update(self):
if not self.is_paused:
scaled_delta_time = self.delta_time * self.game_speed
self.update_game_world(scaled_delta_time)
else:
self.update_paused_state()
def update_game_world(self, scaled_delta_time):
# All game logic here, such as moving objects, checking collisions, etc.
# Example:
# self.player.update(scaled_delta_time)
# self.enemy.update(scaled_delta_time)
# Physics.update(scaled_delta_time)
pass
def update_paused_state(self):
# Update game menu or other paused state interactions
# Example:
# menu.update()
pass
if __name__ == "__main__":
pygame.init()
screen = pygame.display.set_mode((800, 600))
clock = pygame.time.Clock()
game = Game()
running = True
#classic game loop
#better to move it to the game class
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_p:
game.toggle_pause()
game.update()
pygame.display.flip()
clock.tick(60) # Maintain 60 frames per second
pygame.quit()
sys.exit()
class Game
{
private bool isPaused = false;
private float gameSpeed = 1.0f;
private float deltaTime;
public Game()
{
deltaTime = 0.016f; // 60 FPS (16 ms)
}
public void TogglePause()
{
isPaused = !isPaused;
}
public void SetGameSpeed(float speed)
{
gameSpeed = speed;
}
// The main update method that is called every frame
public void Update()
{
if (!isPaused)
{
float scaledDeltaTime = deltaTime * gameSpeed;
// Update game logic
UpdateGameWorld(scaledDeltaTime);
}
else
{
// Game is paused, do not update game logic
// but possibly still update some UI elements if needed
UpdatePausedState();
}
}
private void UpdateGameWorld(float scaledDeltaTime)
{
// All game logic here, such as moving objects, checking collisions, etc.
// Example:
// player.Update(scaledDeltaTime);
// enemy.Update(scaledDeltaTime);
// Physics.Update(scaledDeltaTime);
}
private void UpdatePausedState()
{
// Update game menu or other paused state interactions
// Example:
// menu.Update();
}
}
Мы можем ввести дополнительные локальные шкалы для анимаций. Т.е. начало анимации - это начало временной шкалы данной анимации. Это позволит ускорять, замедлять и останавливать анимации.
Но эти варианты не отвечают на вопрос, как нам создать иллюзию непрерывности игры. Ведь игра работает по кадрам, по шагам.
Если у нас игра - это один большой цикл, состоящий из итераций, мы можем сказать, что каждая итерация - это один кадр. Замерив время начала и конца итерации, получаем время которое потребовалось на отрисовку кадра - и это будет время, прошедшее с момента последнего обновления игры. И использовать его для расчетов при подготовке нового кадра (deltaTime в примере выше).