воскресенье, 28 июня 2026 г.

Графическая сцена “Огонь” на Lazarus 4.6 + SDL 2 + dglOpenGL + Debian 13

Пример кода графической сцены “Огонь” на Lazarus 4.6 + SDL 2 + dglOpenGL + Debian 13. За основу графической сцены “Огонь” взят описаный алгоритм из статьи на сайте https://fabiensanglard.net/doom_fire_psx/

Рисунок 1. Графическая сцена "Огонь"

 

Код файла 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.

 

Комментариев нет:

Отправить комментарий