Исходный код простейшей игры "Космический стражник" построен на базе мультимедиа библиотеки SFML. SFML написана на С++ и предоставляет простой интерфейс для доступа к аппаратным ресурсам ПК. Предназначена библиотека для разработок игр и мультимедийных приложений. Получить доступ к ресурсам библиотеки можно и из среды программирования на C# MS Visual Studio .NET.
SFML (Simple and Fast Multimedia Library) — простая и быстрая бесплатная кроссплатформенная мультимедийная библиотека. Полностью поддерживает объектно-ориентированное программирование. Поддерживается и совершенствуется более 11 лет и авторы сделали многое чтобы программирование игр стало производительным и комфортным. API документация и подробное описание на сайте https://www.sfml-dev.org/
В исходнике использовались несколько модулей: SFML.System обязательный модуль, SFML.Window, SFML.Graphics, SFML.Audio. Для векторной геометрии дополнительно использовался System.Numerics.Vector2 из базовой .NET Standart. Игра условно называется "Космический стражник", имеет простейший игровой процесс: космическая ракета всегда летит к астероиду и разрушает его, спасая Землю от вероятного столкновения. При поражении ракетой небесного тела происходит анимация взрыва с разлетающимися осколками.
Rocket - класс производный от класса Sprite пространства имен SFML.Graphics. Функция класса вычисление направления и полет ракеты к цели.
Asteroid - класс содержащий в себе объект класса SFML.Graphics.Spite. Функции перемещение небесного тела и анимация взрыва.
Window2D - класс производный от RenderWindow принадлежащему пространству имен SFML.Graphics мультимедийной библиотеки SFML Окно для рендеринга полета ракеты к астероиду на фоне пространства космоса.
GameProcess - класс запуска цикла игры.
Program - класс приложения. Имеет главный метод Main автоматически вызывающийся средой .NET Core при запуске приложения. Функция класса запуск приложения.
Далее эти классы описаны подробно. Надо сказать огромное спасибо разработчикам SFML, мультимедиа библиотеки, существенно упрощающей создание реалистичных игровых сюжетов.
Объект класса Rocket визуализирует космическую ракету поражающая опасные астероиды подлетающие к планете. Класс имеет свойства скорости и направления полета, реализует событие поражения цели. Перемещая астероид в пространстве игры, ракета будет разворачиваться и двигаться всегда по направлению к астероиду. При достижении цели вызывается событие взрыва.
При инициализации объекта класса на спрайт одевается текстура, размер ракеты масштабируется до желаемого размера, определяется "ударная точка" и первоначальная позиция боевого снаряда. Кроме того, свойством Origin формируем ось ракеты точно посередине высоты. Теперь у ракеты ударная точка вначале и ось направления движения точно посередине.
public Rocket()
{
// Картинка ракеты.
this.Texture = new Texture(Resource.rocket);
// Масштаб размеров ракеты относительно картинки.
this.Scale = new Vector2f(0.2f, 0.2f);
// Центр удара ракеты, активная точка.
Origin = new Vector2f(GetLocalBounds().Width, GetLocalBounds().Height / 2);
// Первоначальная позиция ракеты.
Position = new Vector2f(150, 50);
}
Разворот ракеты происходит мгновенно, за один кадр, поэтому, для упрощения, угол поворота отсчитывает от неизменяемого базового вектора. Вектор взят в направлении оси Х произвольно, только для более естественного горизонтального положения ракеты в начале событий.
// Базовый вектор от которого отсчитывается угол поворота ракеты
// Высчитывается всегда абсолютный угол.
private readonly Vector2f vectorBaseAngle = new Vector2f(1, 0);
При каждом новом расположении астероида вычисляется нормализованный вектор направления. Единичный вектор направления умножается на скалярную величину скорости и создается вектор скорости. Скалярная величина дает нам вектор скорости на один кадр. На следующий кадр вновь рассчитывается вектор скорости. Так получается синхронизация скорости по отношению к частоте кадрам всего рендеринга. Стабилизация частоты кадров обеспечивает равномерную скорость объекта. Отметим, что в нашем случае частота кадров стабилизирована в 80 кадров в секунду. При условии нехватки аппаратных мощностей частота кадров будет снижаться.
/// <summary>
/// Рисуем спрайт ракеты вместе с анимацией движения.
/// </summary>
public Sprite Draw()
{
// Начальная и конечная позиция ракеты
var start = new System.Numerics.Vector2(Position.X, Position.Y);
var finish = new System.Numerics.Vector2(positionTo.X, positionTo.Y);
// Расчет расстояния от текущей позиции ракеты до цели.
float distance = System.Numerics.Vector2.Distance(start, finish);
if (distance > (scalarVelocity + 0.1f))
{
// Умножая скалярную величину скорости на
// единичный вектор направления получаем вектор скорости.
Vector2f velocity = scalarVelocity * normalDirection;
// Высчитываем новую позицию для каждого кадра.
Position += velocity;
}
else
{
// Событие поражение цели.
if (oneEvent == true)
{
ExplosionEvent(this, null);
// Разрешаем только одно событие взрыва при поражении цели.
oneEvent = false;
}
}
return this;
}
Класс Asteroid отвечает за отображение на экране небесного тела, напоминающего огромный камень. Asteroid включает в себя массив спрайтов Sprite[] и массив текстур Texture[] из подключенной программной библиотеки SFML. Каждый элемент массива спрайтов одевается в свою соответствующую картинку. Массив спрайтов с элементами, одетыми в соответствующую текстуру, имеет общую координатную позицию. Таким образом происходит отображение астероида без признаков его осколочной структуры. И только при попадании ракеты в астероид происходит его разрушение, он разделяется на части и каждый осколок летит по своей случайной траектории.
// Вид астероида
private readonly Texture[] texture =
{
new SFML.Graphics.Texture(Resource.asteroid_p1),
new SFML.Graphics.Texture(Resource.asteroid_p2),
new SFML.Graphics.Texture(Resource.asteroid_p3),
new SFML.Graphics.Texture(Resource.asteroid_p4),
new SFML.Graphics.Texture(Resource.asteroid_p5)
};
// Функциональность астероида из библиотеки SFML
private readonly Sprite[] sprite = new Sprite[5];
public Asteroid()
{
for (int i = 0; i < texture.Length; i++)
{
sprite[i] = new Sprite(texture[i])
{
Origin = new Vector2f(texture[i].Size.X / 2, texture[i].Size.Y / 2),
};
}
// Первоначальная позиция
Position(new Vector2f(-300, 0));
}
Рендеринг (визуализация) небесного тела происходит одним из двух состояний астероида. Стандартный вид астероида, когда все части его показываются совместно и образуют целую каменную глыбу. Другое состояние астероида возникает при поражении его ракетой. Срабатывает детонация, части астероида мгновенно отлетают на установленное расстояние и далее происходит анимация последствий взрыва. Осколки хаотично разлетаются в космосе до тех пор, пока пользователь не разместит астероид в новой позиции пространства космоса. За прорисовку астероида в двух состояниях отвечает метод Draw класса Asteroid. Метод Draw принимает принимает один параметр: объект окна приложения RenderWindow.
public void Draw(RenderWindow window)
{
// Детонация
if (showDetonation == true)
{
showDetonation = false;
ShowExplosion(velocityDetonation);
}
// Последствия взрыва
if (showExplosion == true)
{
ShowExplosion(velocityExplosion);
}
// Астероид стандартный
for (int i = 0; i < texture.Length; i++)
{
window.Draw(sprite[i]);
}
}
При поражении ракетой астероида происходит анимация взрыва. Астероид разрывается на части и каждый осколок летит в своем направлении. Использовании случайных чисел в выборе направления движения осколков повышает разнообразие взрывов и повышает естественность визуализации поражения цели. В исходнике этой игры объект астероида состоит из 5-ти частей. При желании можно улучшить естественность взрыва, добавив ещё и анимацию огня при событии взрыва. Это также можно сделать с помощью спрайтовой анимации. Благо всю кропотливую работу низкоуровневого рендеринга берут на себя модули SFML.
// Астероид. Симуляция взрыва. Разлетаются осколки
// Осколки разлетаются, вращаясь, в разные стороны
private void ShowExplosion(float velocity)
{
// 1 летит влево-вверх
int x = Random.Next(-250, 20);
int y = Random.Next(-150, -10);
var direction = System.Numerics.Vector2.Normalize(new System.Numerics.Vector2(x, y));
Vector2f temp = new Vector2f(direction.X * velocity, direction.Y * velocity);
sprite[0].Position += temp;
...
// 5 летит вправо-вверх
x = Random.Next(-20, 250);
y = Random.Next(-250, -50);
direction = System.Numerics.Vector2.Normalize(new System.Numerics.Vector2(x, y));
temp = new Vector2f(direction.X * velocity, direction.Y * velocity);
sprite[4].Position += temp;
for (int i = 0; i < sprite.Length; i++)
{
sprite[i].Rotation += rotationExplosion[i];
}
}
Класс Window2D производится от RenderWindow модуля SFML.Graphics. Window2D наследует методы и свойства базового класса. RenderWindow не имеет конструкторов без параметров, для создания такого конструктора в производном Window2D используем фиксированные настройки базового конструктора. В итоге в производном классе появляется конструктор без параметров. Это сделано с целью минимизации кода при настройке окна.
// Необходимые настройки формируем в конструкторе без параметров
public Window2D() : base(new VideoMode(800, 600, 24), "Космический стражник")
{
texture = new Texture(Resource.bg);
background = new SFML.Graphics.Sprite(texture);
// Ограничение частоты фреймов для стабилизации скорости
// рендеринга сюжета игры
base.SetFramerateLimit(80);
base.Resized += Window2D_Resized;
base.Closed += Window2D_Closed;
}
Первоначальные параметры рендеринга (визуализации): размер спрайта заднего фона и камера проецирования, по размеру, точно соответствуют размерам окна приложения. При изменении пользователем размеров окна игры эти параметры становятся неподходящими и автоматически они не подгоняются. Чтобы визуальная и координатная точность всегда соответствовала размерам окна визуализации необходимо создать метод, корректирующий эти параметры. При событии изменения размеров окна для параметра заднего фона вычисляется вектор масштабирования и спрайт заднего фона корректируется под новые размеры. Для параметра камера вида вновь назначается ширина и высота размера проецирования.
private void Window2D_Resized(object sender, SizeEventArgs e)
{
// Вычисление вектора масштабирования для спрайта заднего фона
float i = (float)Convert.ToDouble(e.Width);
float j = (float)Convert.ToDouble(e.Height);
float x = i / background.GetLocalBounds().Width;
float y = j / background.GetLocalBounds().Height;
// Масштабируем размеры фона под новый размер окна
background.Scale = new SFML.System.Vector2f(x, y);
// Размер угла просмотра (камеры) также подстраиваем
// под новые размеры окна приложения.
// Необходимо для корректировки координат объектов.
var view = new View(new FloatRect(0, 0, e.Width, e.Height));
base.SetView(view);
}
Класс запуска окна игры. Здесь обрабатываются стандартные события приложения нажатия клавиш клавиатуры и нажатия кнопок мыши. Обрабатывается событие взрыва вызывая анимацию взрыва с реальным звуком. Данному классу принадлежит и метод цикла игры Run(...).
class GameProcess
{
// Для звука взрыва в реальном времени
private readonly Sound sound = new Sound();
private readonly Window2D window2D = new Window2D();
private readonly Rocket rocket = new Rocket();
private readonly Asteroid asteroid = new Asteroid();
public GameProcess()
{
window2D.MouseButtonReleased += Window2D_MouseButtonReleased;
window2D.KeyReleased += Window2D_KeyReleased;
rocket.ExplosionEvent += Rocket_ExplosionEvent;
var soundBuffer = new SoundBuffer(Resource.explosion);
sound.SoundBuffer = soundBuffer;
}
private void Rocket_ExplosionEvent(object sender, EventArgs e)
{
sound.Play();
asteroid.StartExplosion();
}
private void Window2D_KeyReleased(object sender, KeyEventArgs e)
{
// При нажатии любой клавиши окно закрывается
((SFML.Graphics.RenderWindow)sender).Close();
}
private void Window2D_MouseButtonReleased(object sender, MouseButtonEventArgs e)
{
// Отправка новых координат объекту ракеты.
// Для расчёта нового направления движения.
rocket.ComputeDirection(new Vector2f(e.X, e.Y));
// Изменение позиции астероида.
asteroid.Position(new Vector2f(e.X, e.Y));
}
// Запуск игры
public void Run()
{
while (window2D.IsOpen == true)
{
// Обработка очереди событий
window2D.DispatchEvents();
// Самым первым рисуем фон
window2D.DrawBG();
// Затем ракету
window2D.Draw(rocket.Draw());
// Астероид должен перекрывать ракету
asteroid.Draw(window2D);
// Показываем кадр всего что подготовлено
window2D.Display();
}
}
}
Класс Program предназначен для запуска приложения посредством входной точки метода Main(). Метод Main() - это начальный и завершающий этапы управления программой, объявлен без параметров. Методом Run()объекта GameWindow запускается бесконечный цикл рендеринга фреймов игры.
class Program
{
static void Main()
{
var GameProcess = new GameProcess();
GameProcess.Run();
}
}