Sources.RU Magazine Поиск по журналу
 

TopList

Полезные функции и процедуры: часть 1

Автор: OSokin

В этой и следующих статьях я расскажу о некоторых полезностях. Профи они ни к чему, но новичкам могут пригодиться. Здесь рассмотрены многие разделы программирования: математика, графика, сети и т.д. Сразу оговорюсь, что написано всё на Delphi. И еще: то, в чем я не уверен на 99%, я приводить не буду. Скорее всего, будут только уже использовавшиеся участки кода. Надеюсь, что кому-нибудь это пригодится.


Глава 1. Математические функции и процедуры.

Математика - сложная, но требуемая в программировании вещь. Тот, кто плохо знает математику, вряд ли сможет написать даже простой шутер - нужны поворот за мышью, стрельба, движение... Это все требует тригонометрических расчетов и многого другого. В общем, смотрите.

1. Константы.

В некоторых функциях нельзя обойтись без этих констант. Хотя можно использовать их альтернативы, например, функцию DegToRad, их использование в чем-то быстрее (не нужно дополнительное деление, к примеру).

const
  PiDiv2 = Pi / 180;

Константа PiDiv2 используется для перевода градусов в радианы и обратно. Для первого умножайте градусы на это число, для второго - делите.

2. Поворот точки.

Самая простая вещь. На эту тему говорилось много, но вдруг кто не знает.

function RotateX(Angle, X, Y: Real; Xc: Real = 0; Yc: Real = 0): Real;
var
  Radians: Real;
begin
  Radians := Angle * PiD2;
  RotateX := (X - Xc) * Cos(Radians) - (Y - Yc) * Sin(Radians) + Xc;
end;

function RotateY(Angle, X, Y: Real; Xc: Real = 0; Yc: Real = 0): Real;
var
  Radians: Real;
begin
  Radians := Angle * PiD2;
  RotateY := (Y - Yc) * Cos(Radians) + (X - Xc) * Sin(Radians) + Yc;
end;

function RotatePoint(Angle: Real; Point: TPoint; Xc: Real = 0; Yc: Real = 0): TPoint;
begin
  RotatePoint.X := Round(RotateX(Angle, Point.X, Point.Y, Xc, Yc));
  RotatePoint.Y := Round(RotateY(Angle, Point.X, Point.Y, Xc, Yc));
end;

Здесь X и Y или Point - координаты точки, Xc и Yc - координаты центра, вокруг которого точку нужно повернуть на угол Angle. При повороте не забывайте, что в качестве параметров должны быть координаты не повернутой точки (а то бывает, что повернут по оси X и при повороте по Y используют эту координату, а потом удивляются - почему глючит).

3. Угол между точками.

Это немного сложнее, но тоже легко - на уровне тригонометрии девятого класса. В основном используется для поворота игрока за курсором мыши и т.п.

function GetAngle(X, Y: Real): Real;
begin
  GetAngle := -(ArcSin(X / Sqrt((X * X + Y * Y))))) / PiDiv2;
end;

Следует отметить, что точка должна находиться относительно в центре координат. Таким образом, для получения угла между точками нужно подставить значения (X - X0), (Y - Y0).

4. Траектория.

Иногда требуется найти позицию тела, движущегося по определенной траектории, в момент времени. Для этого существует специальная функция:

function GetTrajecPos(X, Y, Speed, Angle, Time: Real): TPoint;
begin
  GetTrajecPos.X := Round(X + Speed * Cos(Angle) * Time);
  GetTrajecPos.Y := Round(Y + Speed * Sin(Angle) * Time + Sqr(Pi * Time) / 2);
end;

Что такое X, Y, Speed, Angle и Time - это, я думаю, и так понятно. Как можно заметить, тут не указана масса тела, однако использовать формулу можно и без нее - уменьшать скорость движения тела обратно пропорционально массе.

5. Псевдослучайные числа.

Случайных чисел в природе не существует - так учит нас математика. Генераторы псевдослучайных чисел существуют практически во всех языках программирования. Но иногда целесообразнее пользоваться своим собственным генератором, нежели стандартным. Для рассказа о том, как его написать, приведу одну мессагу из эхи FIDO7 RU.HACKER за далекий 1996 год:


- Area: RU.HACKER ------------------------------------------------------------
  Msg#: 232                                          Date: 11 Feb 96  18:27:00
  From: Vladimir Gorpenko                            Read: No     Replied: No 
    To: Tolya Zherdev                                Mark:                     
  Subj: random number
------------------------------------------------------------------------------
   Здpавствуй, Tolya!

08 Feb 96 10:13 Tolya Zherdev писал Porfiriev Sergey следующее:

 TZ> Для pавномеpного pаспpеделения:
 TZ> X(I+1)=F{11X(I)+Pi}, пpи X(0)=0.5 дает около 8000 не повтоpяющихся чисел.

  8000 - это что-то очень плохо. Hа 16-pазpядных целых должны получаться все
65536 неповтоpяющихся чисел.

   А что, наpод Кнута совсем читать не хочет?

   Вот pецепт (из "выводов" к pазделу о генеpатоpах псевдослучайных чисел):

 "наилучший" и "пpостейший" датчик случайных чисел получается по фоpмуле

      X(I+1)=(a*X(I)+c) mod m

   Умножение должно быть точным (без окpугления и тому подобного).

   Пpи выбоpе X(0),a,c и m необходимо соблюдать некотоpые пpавила и
использовать случайные числа осмысленно, pуководствуясь следующими пpинципами.

i) Число X(0) может быть пpоизвольным.

ii) Число m должно быть велико. Удобно выбиpать его pавным pазмеpу слова
вычислительной машины (Кнут явно имеет в виду 2**k, где k - pазpядность слова
или pазpядность слова за вычетом знакового pазpяда - пpим. Го), поскольку пpи
этом эффективно вычисляется (a*X+c) mod m.

iii) Если m пpедставляет собой степень двойки (т.е. если используется машина,
pаботающая в двоичной системе счисления), выбеpите a таким, чтобы a mod 8 = 5.
Пpи таком выбоpе величины a, пpи условии, что c выбиpается описанным ниже
способом, гаpантиpуется, что датчик случайных чисел даст все m возможных
pазличных значений X, пpежде чем они начнут повтоpяться и, кpоме того,
гаpантиpуется  высокая "мощность".

iv) Множитель a должен пpевосходить величину Sqrt(m), желательно, чтобы он был
больше m/100, но меньше m-Sqrt(m). Последовательность pазpядов в двоичном
пpедставлении a не должна иметь пpостого, pегуляpного вида. Высказанных
сообpажений обычно бывает достаточно, но если датчик случайных чисел
используется интенсивно, множитель a, кpоме того, следует выбиpать так, чтобы
удовлетвоpялся "спектpальный тест".

v) Постоянная c должна быть pавна нечетному числу. Желательно выбиpать c таким
обpазом, чтобы отношение c/m было бы пpиблизительно pавно величине
0.2113248654051871.

vi) Менее значимые pазpяды X не очень хоpоши, лучше pассматpивать X как дpобь
X/m в интеpвале между 0 и 1.

   За обоснованием, pазъяснением непонятных слов, методами пpовеpки датчиков -
пожалуйста, к Кнуту.

 TZ> Hу а начальную инициализацию фоpмулы (Hапp. X(0) или I0) делаешь в
 TZ> зависимости от текущего вpемени.

   Это точно, только хоpошо бы сpазу кpутануть генеpатоp хоть pазок, чтобы
pазбpос пеpвых значений не так кучковался пpи последовательных запусках.

   С уважением,
      Vladimir

-!- GoldED/386 2.42.G0614
 ! Origin: Огоpошенный сyдьбою, ты все же не отчаивайся! (fidonet 2:5020/157.6)

Сначала кажется, что числа для подобного генератора найти сложно, однако можно удовлетворить практически все эти условия:

m = 65536;
a = 35789;
c = 13849;

Так получаем формулу: X(I+1) = (35789 * X(I) + 13849) mod 65536.

Таким образом, можно написать свою процедуру Randomize и свою функцию Random:

var
  OldX: Integer;

procedure Randomize;
begin
  OldX := GetTickCount;
end;

function Random(I: Integer): Integer;
var
  X: Integer;
begin
  X := (35789 * OldX + 13849) mod 65536;
  OldX := X;
  Result := X mod I;
end;

Естественно, что такая функция Random будет поддерживать только 16-битные числа, однако на практике этого в 90% случаев достаточно. Для поддержки больших чисел нужно будет подбирать новые значения a и c - m тогда должно соответствовать количеству байт под число.


Глава 2. Графические функции и процедуры.

Графика - тоже серьезная вещь. Например, новичок в API не сможет загрузить битмап из файла, если не будет знать его размеры. От "мерцания" он тоже вряд ли сможет избавиться.

1. Поверхности.

О создании поверхностей можно почитать в статье "Поверхности: как это делается". Использование поверхностей избавляет от "мерцания".

2. Загрузка битмапа.

Рисунки в формате BMP используются практически везде. Формат нетрудный, поддерживается Windows, легко сжимается и прочая, и прочая. Но стандартные функции Windows позволяют загрузить его из файла, только если будет известен размер. Для нормальной загрузки битмапа из файла может послужить следующая функция:

function LoadBitmapFromFile(FileName: String): HBITMAP;
var
  // Файл для загрузки заголовка битмапа.
  F: File;
  // Заголовочные структуры.
  BitmapFileHeader : TBITMAPFILEHEADER;
  BitmapInfoHeader : TBITMAPINFOHEADER;
  // Счетчик чтения файла. Применяется для проверки.
  ReadCount : DWORD;
begin
  LoadBitmapFromFile := 0;
  // Открытие файла и установка на начало.
  AssignFile(F, FileName);
  Reset(F, 1);
  // Чтение заголовка TBitmapFileHeader.
  BlockRead(F, BitmapFileHeader, SizeOf(TBitmapFileHeader), ReadCount);
  if (ReadCount <> SizeOf(TBitmapFileHeader)) then
  begin //Ошибка чтения.
    CloseFile(F);
    Exit;
  end;
  // Чтение заголовка TBitmapInfoHeader.
  BlockRead(F, BitmapInfoHeader, SizeOf(TBitmapInfoHeader), ReadCount);
  if (ReadCount <> SizeOf(TBitmapInfoHeader)) then
  begin //Ошибка чтения.
    CloseFile(F);
    Exit;
  end;
  CloseFile(F);
  with BitmapInfoHeader do
  begin
    Result := LoadImage(HInstance, PChar(FileName), IMAGE_BITMAP,
      biWidth, biHeight, LR_LOADFROMFILE);
    SetBitmapDimensionEx(Result, biWidth, biHeight, nil);
  end;
end;

При возникновении ошибки функция вернет 0. Я частично взял эту функцию из статьи Мироводина Дмитрия "Низкоуровневая загрузка растра", которую можно найти на DelphiGFX. Правда, здесь больше преимуществ: меньше размер, один и тот же код для разных типов и пр.

3. Отображение рисунка частями (pattern drawing).

Часто требуется отобразить не весь рисунок, а только какую-либо его часть. Примеров куча: графические редакторы, игры и т. п.

Для следующего примера потребуется переменная Brush типа HBrush, инициализированная с помощью функции CreatePatternBrush(Handle), где Handle - это описатель битмапа. Удаляется эта кисть с помощью DeleteObject(Brush). Можно это делать прямо в процедуре, но тогда сильно снизится скорость. Также перед этим должна быть вызвана функция SetBitmapDimensionEx для того, чтобы процедура могла получить размер рисунка. Эту функцию (SetBitmapDimensionEx) достаточно вызвать только один раз, при изменении размеров рисунка.

procedure DrawPattern(DC: HDC; X, Y: Integer; PatWidth, PatHeight: Integer;
  Brush: HBRUSH; Bitmap: HBITMAP; Pattern: Integer = -1);
var
  BmpSize: SIZE;
  OldObj: HGDIOBJ;
  OldOrg: TPoint;
begin
  OldObj := SelectObject(DC, Brush);
  //Получить размер рисунка.
  GetBitmapDimensionEx(Bitmap, BmpSize);
  if (Pattern = -1) then
  begin //Отобразить весь рисунок.
    SetBrushOrgEx(DC, X, Y, @OldOrg);
    PatBlt(DC, X, Y, BmpSize.cX, BmpSize.cY, PATCOPY);
  end
  else
  begin //Отобразить только часть рисунка.
    SetBrushOrgEx(DC, X - (Pattern mod (BmpSize.cX div PatWidth)) * PatWidth,
      Y - (Pattern div (BmpSize.cX div PatWidth)) * PatHeight, @OldOrg);
    PatBlt(DC, X, Y, PatWidth, PatHeight, PATCOPY);
  end;
  SetBrushOrgEx(DC, OldOrg.X, OldOrg.Y, nil);
  SelectObject(DC, OldObj);
end;

Теперь пояснения. Windows использует BrushOrg, чтобы рисовать с определенной точки. Так частенько поступают графические редакторы при предпросмотре смещения рисунка. А мы используем это, чтобы отобразить только часть рисунка.

4. Отображение рисунка с поворотом.

Нижеследующий пример дает изображение похуже, чем те же OpenGL или DirectDraw, но зато не требует дополнительных библиотек. Тут есть и еще один большой недостаток: данный код работоспособен только под Windows семейства NT. Если его немного переделать, то можно отображать рисунок частями. Те, кто прочел первую главу, наверняка заметят некоторое сходство - тут использовались те же формулы.

procedure DrawRotated(DC: HDC; Bitmap: HBITMAP; X, Y: Integer; Angle: Real);
var
  TempDC: HDC;
  BmpSize: SIZE;
  Radians: Real;
  Points: array[0..2] of TPoint;
  Xc, Yc: Integer;
begin
  TempDC := CreateCompatibleDC(DC);
  SelectObject(TempDC, Bitmap);
  GetBitmapDimensionEx(Bitmap, BmpSize);
  Radians := Angle * Pi / 180;

  with BmpSize do
  begin
    //Находим центр.
    Xc := X + (cX div 2);
    Yc := Y + (cY div 2);

    //Считаем координаты.
    Points[0].X := Round((X - Xc) * Cos(Radians) - (Y - Yc) * Sin(Radians) + Xc);
    Points[0].Y := Round((Y - Yc) * Cos(Radians) + (X - Xc) * Sin(Radians) + Yc);

    Points[1].X := Round((X + cX - Xc) * Cos(Radians) - (Y - Yc) * Sin(Radians) + Xc);
    Points[1].Y := Round((Y - Yc) * Cos(Radians) + (X + cX - Xc) * Sin(Radians) + Yc);

    Points[2].X := Round((X - Xc) * Cos(Radians) - (Y + cY - Yc) * Sin(Radians) + Xc);
    Points[2].Y := Round((Y + cY - Yc) * cos(Radians) + (X - Xc) * Sin(Radians) + Yc);

    //Выводим.
    PlgBlt(DC, Points, TempDC, 0, 0, cX, cY, 0, 0, 0);
    DeleteDC(TempDC);
  end;
end;

И опять-таки можно сильно оптимизировать этот процесс: TempDC используется лишь для копирования с нее изображения. Каждый раз создавать и удалять контекст устройства - долго. Лучше сразу после загрузки изображения создавать DC и выбирать Bitmap для него, а когда он уже больше не нужен - удалять. Таким же образом можно оптимизировать и предыдущую процедуру:

procedure DrawPattern(DC, TempDC: HDC; X, Y, PatWidth, PatHeight: Integer;
  Bitmap: HBITMAP; Pattern: Integer = -1);
var
  BmpSize: SIZE;
begin
  //Получить размер рисунка.
  GetBitmapDimensionEx(Bitmap, BmpSize);
  with BmpSize do
    if (Pattern = -1) then
      //Отобразить весь рисунок.
      BitBlt(DC, X, Y, cX, cY, TempDC, 0, 0, SRCCOPY)
    else
      //Отобразить только часть рисунка.
      BitBlt(DC, X, Y, PatWidth, PatHeight, TempDC,
        (Pattern mod (cX div PatWidth)) * PatWidth,
        (Pattern div (cX div PatWidth)) * PatHeight, SRCCOPY);
end;

Так получится гораздо быстрее.



 Design by Шишкин Алексей (Лёха)  ©2004-2008 by sources.ru