Пример кода графической сцены “Огонь” на Lazarus 4.6 + SDL 2 + dglOpenGL + Debian 13. За основу графической сцены “Огонь” взят описаный алгоритм из статьи на сайте https://fabiensanglard.net/doom_fire_psx/.
Код файла ogl_p7.lpr:
program ogl_p7;
{$mode objfpc}{$H+}
uses
{$IFDEF UNIX}
cthreads,
{$ENDIF}
Classes, SysUtils, sdl2, sdl2_image, sdl2_ttf, dglOpenGL, SparksSystem
{ you can add units after this };
const
FIRE_WIDTH = 320; // Низкое разрешение для аутентичного просчета физики
FIRE_HEIGHT = 240;
FIRE_SIZE = FIRE_WIDTH * FIRE_HEIGHT;
SCREEN_WIDTH = 800;
SCREEN_HEIGHT = 450;
FPS_TARGET = 60;
FRAME_DELAY = 1000 div FPS_TARGET;
type
TColorRGB = record
R, G, B: Byte;
end;
TFirePalette = array[0..36] of TColorRGB;
TFireBuffer = array[0..FIRE_SIZE - 1] of Byte;
TScreenRGB = array[0..FIRE_SIZE - 1] of TColorRGB;
var
Window: PSDL_Window = nil;
GLContext: TSDL_GLContext = nil;
Event: TSDL_Event;
Running: Boolean = True;
FrameStart, FrameTime: UInt32;
FireBuffer: TFireBuffer; // Буфер индексов [0..36]
ScreenRGB: TScreenRGB; // Буфер для вывода в OpenGL текстуру
TextureID: GLuint; // ID текстуры OpenGL
// Переменные для работы со шрифтами
Font: PTTF_Font = nil;
CurrentPalette: TFirePalette;
WindShift: Integer = 1; // 1 - штиль (строго вверх), 0 - ветер вправо, 2 - ветер влево
// Текущая отображаемая палитра, которая будет плавно меняться
ActiveDisplayPalette: TFirePalette;
// Целевая палитра, к которой мы осуществляем переход
TargetPalette: TFirePalette;
// Переменная для контроля скорости анимации (не зависит от FPS)
LastFrameTime: UInt32 = 0;
const
// 1. КЛАССИЧЕСКАЯ ПАЛИТРА (Огонь)
PaletteClassic: TFirePalette = (
(R: 7; G: 7; B: 7), (R: 31; G: 7; B: 7), (R: 55; G: 7; B: 7),
(R: 79; G: 15; B: 7), (R: 103; G: 15; B: 7), (R: 127; G: 23; B: 7),
(R: 151; G: 23; B: 7), (R: 175; G: 31; B: 7), (R: 199; G: 39; B: 15),
(R: 223; G: 47; B: 15), (R: 223; G: 55; B: 15), (R: 223; G: 63; B: 15),
(R: 215; G: 71; B: 15), (R: 215; G: 79; B: 15), (R: 215; G: 87; B: 15),
(R: 207; G: 95; B: 23), (R: 207; G: 103; B: 23), (R: 207; G: 111; B: 23),
(R: 199; G: 119; B: 23), (R: 199; G: 127; B: 23), (R: 191; G: 135; B: 31),
(R: 191; G: 143; B: 31), (R: 191; G: 151; B: 31), (R: 183; G: 159; B: 31),
(R: 183; G: 167; B: 39), (R: 183; G: 175; B: 39), (R: 175; G: 183; B: 39),
(R: 175; G: 191; B: 39), (R: 175; G: 199; B: 47), (R: 167; G: 207; B: 47),
(R: 167; G: 215; B: 47), (R: 167; G: 223; B: 47), (R: 159; G: 231; B: 47),
(R: 159; G: 239; B: 55), (R: 159; G: 247; B: 55), (R: 151; G: 255; B: 55),
(R: 255; G: 255; B: 255)
);
// 2. СИНЯЯ ПАЛИТРА (Ледяное пламя)
PaletteBlue: TFirePalette = (
(R: 0; G: 0; B: 10), (R: 0; G: 5; B: 30), (R: 0; G: 10; B: 50),
(R: 0; G: 15; B: 70), (R: 0; G: 20; B: 90), (R: 0; G: 25; B: 110),
(R: 0; G: 30; B: 130), (R: 0; G: 40; B: 150), (R: 0; G: 50; B: 170),
(R: 0; G: 60; B: 190), (R: 0; G: 70; B: 200), (R: 0; G: 80; B: 210),
(R: 0; G: 90; B: 220), (R: 0; G: 100; B: 230), (R: 0; G: 110; B: 240),
(R: 0; G: 120; B: 250), (R: 0; G: 130; B: 255), (R: 10; G: 140; B: 255),
(R: 20; G: 150; B: 255), (R: 30; G: 160; B: 255), (R: 40; G: 170; B: 255),
(R: 50; G: 180; B: 255), (R: 60; G: 190; B: 255), (R: 70; G: 200; B: 255),
(R: 80; G: 210; B: 255), (R: 90; G: 220; B: 255), (R: 100; G: 230; B: 255),
(R: 120; G: 240; B: 255), (R: 140; G: 245; B: 255), (R: 160; G: 250; B: 255),
(R: 180; G: 252; B: 255), (R: 200; G: 255; B: 255), (R: 210; G: 255; B: 255),
(R: 220; G: 255; B: 255), (R: 230; G: 255; B: 255), (R: 240; G: 255; B: 255),
(R: 255; G: 255; B: 255)
);
// 3. ЗЕЛЕНАЯ ПАЛИТРА (Некромантия / Кислота)
PaletteGreen: TFirePalette = (
(R: 0; G: 10; B: 0), (R: 0; G: 30; B: 0), (R: 0; G: 50; B: 0),
(R: 0; G: 70; B: 0), (R: 0; G: 90; B: 0), (R: 0; G: 110; B: 0),
(R: 0; G: 130; B: 0), (R: 0; G: 150; B: 0), (R: 10; G: 160; B: 5),
(R: 20; G: 170; B: 10), (R: 30; G: 180; B: 15), (R: 40; G: 190; B: 20),
(R: 50; G: 200; B: 25), (R: 60; G: 210; B: 30), (R: 70; G: 220; B: 35),
(R: 80; G: 230; B: 40), (R: 90; G: 240; B: 45), (R: 100; G: 245; B: 50),
(R: 110; G: 250; B: 60), (R: 120; G: 255; B: 70), (R: 130; G: 255; B: 80),
(R: 140; G: 255; B: 90), (R: 150; G: 255; B: 100), (R: 160; G: 255; B: 110),
(R: 170; G: 255; B: 120), (R: 180; G: 255; B: 135), (R: 190; G: 255; B: 150),
(R: 200; G: 255; B: 165), (R: 210; G: 255; B: 180), (R: 220; G: 255; B: 195),
(R: 225; G: 255; B: 210), (R: 230; G: 255; B: 220), (R: 235; G: 255; B: 230),
(R: 240; G: 255; B: 240), (R: 245; G: 255; B: 245), (R: 250; G: 255; B: 250),
(R: 255; G: 255; B: 255)
);
// 4. ФИОЛЕТОВАЯ ПАЛИТРА (Мистическая магия искажения)
PalettePurple: TFirePalette = (
(R: 10; G: 0; B: 10), (R: 25; G: 0; B: 30), (R: 40; G: 0; B: 50),
(R: 55; G: 0; B: 70), (R: 70; G: 0; B: 90), (R: 85; G: 0; B: 110),
(R: 100; G: 0; B: 130), (R: 115; G: 0; B: 150), (R: 130; G: 10; B: 165),
(R: 145; G: 15; B: 180), (R: 160; G: 20; B: 195), (R: 175; G: 25; B: 210),
(R: 190; G: 30; B: 225), (R: 205; G: 35; B: 240), (R: 215; G: 40; B: 250),
(R: 220; G: 45; B: 255), (R: 225; G: 55; B: 255), (R: 230; G: 70; B: 255),
(R: 235; G: 85; B: 255), (R: 235; G: 100; B: 255), (R: 240; G: 115; B: 255),
(R: 240; G: 130; B: 255), (R: 242; G: 145; B: 255), (R: 242; G: 160; B: 255),
(R: 245; G: 175; B: 255), (R: 245; G: 190; B: 255), (R: 248; G: 200; B: 255),
(R: 248; G: 210; B: 255), (R: 250; G: 220; B: 255), (R: 250; G: 230; B: 255),
(R: 252; G: 235; B: 255), (R: 252; G: 240; B: 255), (R: 253; G: 245; B: 255),
(R: 253; G: 250; B: 255), (R: 254; G: 252; B: 255), (R: 254; G: 254; B: 255),
(R: 255; G: 255; B: 255)
);
procedure InitGL;
begin
if not InitOpenGL then
begin
WriteLn('Критическая ошибка: dglOpenGL не смог инициализироваться!');
Halt(1);
end;
ReadExtensions;
// Настройка 2D ортографической проекции под размеры комнаты GameMaker
glViewport(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
// Лево=0, Право=800, Низ=450, Верх=0 (Инвертируем Y, как в GameMaker)
glOrtho(0, SCREEN_WIDTH, SCREEN_HEIGHT, 0, -1, 1);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
// Базовые параметры рендеринга
glClearColor(0.0, 0.0, 0.0, 1.0); // Черный фон из rm_test.room.gmx
glDisable(GL_DEPTH_TEST); // 2D режим, глубина не нужна
// Включаем прозрачность для красивого наложения букв текста
// Включаем смешивание цветов (альфа-канал) для прозрачного фона текста
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
end;
procedure SpreadFire(SrcIndex: Integer); inline;
var
Rand, TargetIndex, Decay: Integer;
begin
Rand := Random(4);
// ВМЕСТО "+ 1" подставляем переменную WindShift
TargetIndex := SrcIndex - FIRE_WIDTH - Rand + WindShift;
if (TargetIndex < 0) or (TargetIndex >= FIRE_SIZE) then Exit;
Decay := Rand and 1;
if FireBuffer[SrcIndex] >= Decay then
FireBuffer[TargetIndex] := FireBuffer[SrcIndex] - Decay
else
FireBuffer[TargetIndex] := 0;
end;
procedure AnimatePalette;
var
I: Integer;
CurrentTime, DeltaTime: UInt32;
Factor: Single;
DiffR, DiffG, DiffB: Single;
begin
CurrentTime := SDL_GetTicks();
if LastFrameTime = 0 then LastFrameTime := CurrentTime;
// Получаем время, прошедшее с прошлого кадра в секундах
DeltaTime := CurrentTime - LastFrameTime;
LastFrameTime := CurrentTime;
// Factor определяет скорость перехода.
// 1.0 = переход за 1 секунду. 0.5 = переход за 2 секунды.
Factor := (DeltaTime / 1000.0) * 1.0;
if Factor > 1.0 then Factor := 1.0;
if Factor <= 0.0 then Exit;
// Проходим по всем 37 цветам палитры
for I := 0 to 36 do
begin
// Вычисляем разницу между текущим и целевым цветом
DiffR := TargetPalette[I].R - ActiveDisplayPalette[I].R;
DiffG := TargetPalette[I].G - ActiveDisplayPalette[I].G;
DiffB := TargetPalette[I].B - ActiveDisplayPalette[I].B;
// Плавно приближаем текущий цвет к целевому
ActiveDisplayPalette[I].R := Round(ActiveDisplayPalette[I].R + (DiffR * Factor));
ActiveDisplayPalette[I].G := Round(ActiveDisplayPalette[I].G + (DiffG * Factor));
ActiveDisplayPalette[I].B := Round(ActiveDisplayPalette[I].B + (DiffB * Factor));
end;
end;
procedure Fire_Init;
var
i, StartOfLastLine: Integer;
begin
Randomize;
// Изначально отображаемая и целевая палитры одинаковы
ActiveDisplayPalette := PaletteClassic;
TargetPalette := PaletteClassic;
CurrentPalette := PaletteClassic; // Если эта переменная еще используется, оставляем
// Очищаем буфер (Часть 2 — все черное)
FillChar(FireBuffer, SizeOf(FireBuffer), 0);
FillChar(ScreenRGB, SizeOf(ScreenRGB), 0);
// Заполняем нижнюю строчку источником огня (Часть 2 — значение 36)
StartOfLastLine := (FIRE_HEIGHT - 1) * FIRE_WIDTH;
for i := StartOfLastLine to FIRE_SIZE - 1 do
FireBuffer[i] := 36;
// Создаем динамическую текстуру в OpenGL
glGenTextures(1, @TextureID);
glBindTexture(GL_TEXTURE_2D, TextureID);
// Включаем GL_LINEAR для современного "гладкого" вида пламени
// Если захотите ретро-пиксели, смените GL_LINEAR на GL_NEAREST
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
// Выделяем память под текстуру в видеокарте
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, FIRE_WIDTH, FIRE_HEIGHT, 0, GL_RGB, GL_UNSIGNED_BYTE, nil);
end;
procedure Fire_Update;
var
x, y, Index: Integer;
begin
// Просчет физики огня снизу вверх (Часть 3)
// Начинаем с y = 1 (строка 0 сверху не обновляется, как и самая нижняя)
for x := 0 to FIRE_WIDTH - 1 do
begin
for y := 1 to FIRE_HEIGHT - 1 do
begin
SpreadFire(y * FIRE_WIDTH + x);
end;
end;
// Переводим индексы [0..36] в реальные RGB цвета из палитры
for Index := 0 to FIRE_SIZE - 1 do
begin
ScreenRGB[Index] := ActiveDisplayPalette[FireBuffer[Index]];
end;
// Обновляем текстуру на видеокарте новыми данными
glBindTexture(GL_TEXTURE_2D, TextureID);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, FIRE_WIDTH, FIRE_HEIGHT, GL_RGB, GL_UNSIGNED_BYTE, @ScreenRGB[0]);
end;
procedure Fire_Render;
begin
// Очищаем экран (черный фон)
glClearColor(0.0, 0.0, 0.0, 1.0);
glClear(GL_COLOR_BUFFER_BIT);
glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, TextureID);
// Рисуем прямоугольник в пиксельных координатах glOrtho (0..800, 0..450)
glBegin(GL_QUADS);
glTexCoord2f(0.0, 0.0); glVertex2f(0, 0); // Верх-лево
glTexCoord2f(1.0, 0.0); glVertex2f(SCREEN_WIDTH, 0); // Верх-право
glTexCoord2f(1.0, 1.0); glVertex2f(SCREEN_WIDTH, SCREEN_HEIGHT); // Низ-право
glTexCoord2f(0.0, 1.0); glVertex2f(0, SCREEN_HEIGHT); // Низ-лево
glEnd;
glDisable(GL_TEXTURE_2D);
end;
// ИНТЕГРАЦИЯ ВАШЕЙ СИСТЕМЫ ПЕЧАТИ (Части 2, 3, 4, 5 вашего примера)
procedure DrawHints;
var
W, H: Integer;
Lines: array of string;
I: Integer;
Surface: PSDL_Surface;
TexID: GLuint;
Color: TSDL_Color;
XPos, YPos: Integer;
BgHeight: Integer;
MaxWidth: Integer;
LineHeights: array of Integer;
TotalHeight: Integer;
ConvSurface: PSDL_Surface;
StringW, StringH: Integer;
begin
if Font = nil then Exit;
SDL_GetWindowSize(Window, @W, @H);
// Наш русский текст подсказок выбора магии
Lines := [
'Выбор магии стихий: [1] Огонь [2] Лед [3] Кислота [4] Бездна',
'Управление ветром: [<->] Вправо [^] Штиль',
'ESC - Выход из демо-сцены'
];
// Полностью безопасный расчет размеров без вызова Render функций
SetLength(LineHeights, Length(Lines));
MaxWidth := 0;
TotalHeight := 0;
for I := 0 to High(Lines) do
begin
StringW := 0;
StringH := 0;
// Быстро узнаем ширину и высоту строки без создания SDL_Surface
if TTF_SizeUTF8(Font, PChar(Lines[I]), @StringW, @StringH) = 0 then
begin
LineHeights[I] := StringH;
if StringW > MaxWidth then
MaxWidth := StringW;
TotalHeight := TotalHeight + StringH + 5;
end
else
begin
LineHeights[I] := 0;
end;
end;
// --- Сохраняем состояние OpenGL ---
glPushAttrib(GL_ENABLE_BIT or GL_TEXTURE_BIT or GL_CURRENT_BIT);
glMatrixMode(GL_PROJECTION);
glPushMatrix;
glLoadIdentity;
glOrtho(0, W, H, 0, -1, 1);
glMatrixMode(GL_MODELVIEW);
glPushMatrix;
glLoadIdentity;
glDisable(GL_DEPTH_TEST);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
XPos := 10;
YPos := 10;
// --- Полупрозрачная чёрная подложка ---
BgHeight := TotalHeight + 10;
glDisable(GL_TEXTURE_2D);
glColor4f(0.0, 0.0, 0.0, 0.6);
glBegin(GL_QUADS);
glVertex2f(XPos, YPos);
glVertex2f(XPos + MaxWidth + 20, YPos);
glVertex2f(XPos + MaxWidth + 20, YPos + BgHeight);
glVertex2f(XPos, YPos + BgHeight);
glEnd;
// --- Рендерим каждую строку текста ---
YPos := YPos + 8;
// === ИСПРАВЛЕНИЕ: Обязательно задаем белый цвет для шрифта! ===
Color.r := 255; Color.g := 255; Color.b := 255; Color.a := 255;
for I := 0 to High(Lines) do
begin
Surface := TTF_RenderUTF8_Blended(Font, PChar(Lines[I]), Color);
if Surface <> nil then
begin
ConvSurface := SDL_ConvertSurfaceFormat(Surface, SDL_PIXELFORMAT_ABGR8888, 0);
if ConvSurface <> nil then
begin
glGenTextures(1, @TexID);
glBindTexture(GL_TEXTURE_2D, TexID);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glPixelStorei(GL_UNPACK_ROW_LENGTH, ConvSurface^.pitch div 4);
// Загружаем текстуру текста
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, ConvSurface^.w, ConvSurface^.h, 0, GL_RGBA, GL_UNSIGNED_BYTE, ConvSurface^.pixels);
// === ИСПРАВЛЕНИЕ: МГНОВЕННЫЙ СБРОС ПАРАМЕТРОВ РАСПАКОВКИ ===
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); // Сбрасываем длину строки в авто-режим
glPixelStorei(GL_UNPACK_ALIGNMENT, 4); // Возвращаем стандартное выравнивание 4 байта
// ==========================================================
glEnable(GL_TEXTURE_2D);
glColor4f(1.0, 1.0, 1.0, 1.0);
glBegin(GL_QUADS);
glTexCoord2f(0.0, 0.0); glVertex2f(XPos + 10, YPos);
glTexCoord2f(1.0, 0.0); glVertex2f(XPos + 10 + Surface^.w, YPos);
glTexCoord2f(1.0, 1.0); glVertex2f(XPos + 10 + Surface^.w, YPos + Surface^.h);
glTexCoord2f(0.0, 1.0); glVertex2f(XPos + 10, YPos + Surface^.h);
glEnd;
glDeleteTextures(1, @TexID);
SDL_FreeSurface(ConvSurface);
end;
YPos := YPos + Surface^.h + 5;
SDL_FreeSurface(Surface);
end;
end;
// --- Восстанавливаем состояние OpenGL ---
glPopMatrix;
glMatrixMode(GL_PROJECTION);
glPopMatrix;
glMatrixMode(GL_MODELVIEW);
glPopAttrib;
end;
// Инициализация подсистемы шрифтов (Часть 1 вашего примера)
procedure InitFont;
begin
if TTF_Init() = -1 then
begin
WriteLn('Ошибка TTF_Init: ', TTF_GetError());
Exit;
end;
Font := TTF_OpenFont('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', 16);
if Font = nil then
WriteLn('Ошибка загрузки шрифта: ', TTF_GetError());
end;
procedure Fire_Free;
begin
if TextureID <> 0 then
glDeleteTextures(1, @TextureID);
end;
begin
// Инициализация SDL2
if SDL_Init(SDL_INIT_VIDEO) < 0 then
begin
WriteLn('Ошибка SDL_Init: ', SDL_GetError());
Halt(1);
end;
// Инициализируем шрифты из вашего примера
InitFont;
// Настройка атрибутов OpenGL контекста
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 1);
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
// Создание окна
Window := SDL_CreateWindow(
'Doom Fire Example Port (Lazarus + SDL2 + OpenGL)',
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
SCREEN_WIDTH, SCREEN_HEIGHT,
SDL_WINDOW_OPENGL or SDL_WINDOW_SHOWN
);
if Window = nil then
begin
WriteLn('Не удалось создать окно: ', SDL_GetError());
SDL_Quit();
Halt(1);
end;
GLContext := SDL_GL_CreateContext(Window);
if GLContext = nil then
begin
WriteLn('Не удалось создать OpenGL контекст: ', SDL_GetError());
SDL_DestroyWindow(Window);
SDL_Quit();
Halt(1);
end;
InitGL;
Fire_Init;
Sparks_Init;
// Главный игровой цикл (60 FPS Game Loop)
while Running do
begin
FrameStart := SDL_GetTicks();
// Обработка ввода и системных событий Linux
while SDL_PollEvent(@Event) <> 0 do
begin
if Event.type_ = SDL_QUITEV then Running := False;
if Event.type_ = SDL_KEYDOWN then
begin
case Event.key.keysym.sym of
SDLK_ESCAPE: Running := False;
// Выбор магии (ваши старые кнопки)
// Изменяем: задаем ЦЕЛЕВУЮ палитру, к которой OpenGL начнет плавно стремиться
SDLK_1, SDLK_KP_1: TargetPalette := PaletteClassic;
SDLK_2, SDLK_KP_2: TargetPalette := PaletteBlue;
SDLK_3, SDLK_KP_3: TargetPalette := PaletteGreen;
SDLK_4, SDLK_KP_4: TargetPalette := PalettePurple;
// === УПРАВЛЕНИЕ ВЕТРОМ ===
// === ИСПРАВЛЕННОЕ, ИНТУИТИВНОЕ УПРАВЛЕНИЕ ВЕТРОМ ===
SDLK_LEFT: WindShift := 0; // Огонь отклоняется ВЛЕВО
SDLK_RIGHT: WindShift := 2; // Огонь отклоняется ВПРАВО
SDLK_UP: WindShift := 1; // Сброс ветра (ШТИЛЬ, строго вверх)
end;
end;
end;
// Рендеринг (Аналог Draw в GameMaker)
glClear(GL_COLOR_BUFFER_BIT);
glLoadIdentity();
// ВАЖНО: Перед рендерингом огня всегда принудительно возвращаем белый цвет,
// чтобы текстура не наследовала черный цвет от плашки из предыдущего кадра!
glColor4f(1.0, 1.0, 1.0, 1.0);
Fire_Update; // Считаем физику пламени
Fire_Render; // Рисуем прямоугольник с текстурой огня
// === ДОБАВЛЯЕМ ВЫЗОВ ТУТ ===
AnimatePalette; // Плавно пересчитываем цвета палитры на текущий кадр
// === ДОБАВЛЯЕМ ИСКРЫ ТУТ ===
Sparks_Update(WindShift); // Обновляем физику с учетом направления ветра
Sparks_Render(SCREEN_WIDTH, SCREEN_HEIGHT); // Рисуем искры поверх огня
// ===========================
// Вызываем вашу процедуру подсказок с русским текстом
DrawHints;
SDL_GL_SwapWindow(Window); // Меняем буферы SDL2
// Ограничение кадров до 60 FPS
FrameTime := SDL_GetTicks() - FrameStart;
if FrameTime < FRAME_DELAY then SDL_Delay(FRAME_DELAY - FrameTime);
end;
// Освобождение памяти
if Font <> nil then TTF_CloseFont(Font);
TTF_Quit();
// Очистка памяти
Fire_Free;
SDL_GL_DeleteContext(GLContext);
SDL_DestroyWindow(Window);
SDL_Quit();
end.
->
Код файла sparksystem.pas:
unit SparksSystem;
{$mode objfpc}{$H+}
interface
uses
dglOpenGL, SysUtils;
const
MAX_SPARKS = 150; // Максимальное количество искр на экране
type
TSpark = record
X, Y: Single; // Позиция искры
VX, VY: Single; // Скорость по X и Y
Life: Single; // Время жизни / Прозрачность (от 1.0 до 0.0)
Decay: Single; // Скорость угасания
Size: Single; // Размер пикселя
end;
// Инициализация системы искр
procedure Sparks_Init;
// Обновление физики искр (вызывать каждый кадр перед рендерингом)
procedure Sparks_Update(WindShift: Integer);
// Отрисовка искр в OpenGL
procedure Sparks_Render(ScreenWidth, ScreenHeight: Integer);
implementation
var
Sparks: array[0..MAX_SPARKS - 1] of TSpark;
procedure Sparks_Init;
var
I: Integer;
begin
// Изначально все искры «мертвы» (Life = 0)
for I := 0 to MAX_SPARKS - 1 do
Sparks[I].Life := 0.0;
end;
procedure Sparks_Update(WindShift: Integer);
var
I: Integer;
WindForce: Single;
begin
// Рассчитываем силу ветра на основе вашей переменной WindShift
// WindShift = 0 (ветер влево), 1 (нет ветра), 2 (ветер вправо)
WindForce := (WindShift - 1) * 0.4;
for I := 0 to MAX_SPARKS - 1 do
begin
// Если искра мертва, генерируем её заново у основания огня
if Sparks[I].Life <= 0.0 then
begin
Sparks[I].X := Random(800); // Случайное место по ширине экрана
Sparks[I].Y := 410 + Random(40); // Чуть выше самого низа (в зоне пламени)
Sparks[I].VX := (Random(100) - 50) / 100.0; // Небольшой хаос влево/вправо
Sparks[I].VY := -(1.5 + Random(200) / 100.0); // Скорость взлета вверх (отрицательный Y)
Sparks[I].Life := 0.6 + (Random(40) / 100.0); // Случайная яркость от 0.6 до 1.0
Sparks[I].Decay := 0.005 + (Random(10) / 1000.0); // Скорость остывания
Sparks[I].Size := 1.5 + Random(2); // Разные размеры искр для объема
end
else
begin
// Физика движения искры
Sparks[I].X := Sparks[I].X + Sparks[I].VX + WindForce;
Sparks[I].Y := Sparks[I].Y + Sparks[I].VY;
// Небольшое хаотичное покачивание в воздухе
Sparks[I].VX := Sparks[I].VX + (Random(100) - 50) / 500.0;
// Уменьшаем время жизни (искра остывает и гаснет)
Sparks[I].Life := Sparks[I].Life - Sparks[I].Decay;
end;
end;
end;
procedure Sparks_Render(ScreenWidth, ScreenHeight: Integer);
var
I: Integer;
begin
// Сохраняем состояние, отключаем текстуры, чтобы рисовать чистые точки
glPushAttrib(GL_ENABLE_BIT or GL_CURRENT_BIT);
glDisable(GL_TEXTURE_2D);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE); // Режим смешивания "Add" (эффект свечения)
for I := 0 to MAX_SPARKS - 1 do
begin
if Sparks[I].Life > 0.0 then
begin
// Задаем размер точки в пикселях
glPointSize(Sparks[I].Size);
// Задаем цвет искры.
// Желто-оранжевый цвет, который плавно угасает через Альфа-канал (Life)
// Искры будут унаследовать яркость от своего времени жизни
glColor4f(1.0, 0.6 + (Sparks[I].Life * 0.4), 0.2, Sparks[I].Life);
glBegin(GL_POINTS);
glVertex2f(Sparks[I].X, Sparks[I].Y);
glEnd;
end;
end;
glPopAttrib;
end;
end.








