Программирование игр для Windows. Советы профессионала

       

Создание звездного неба с использованием целых чисел


Теперь поговорим о том, как можно создать звездное небо используя только целые числа. Мы могли бы для получения более точной картины использовать числа с плавающей запятой, такие, например, как 2,5. Однако мы знаем, что это снижает производительность системы. Нам же требуется создать и визуализировать 50-100 звезд как можно быстрее, чтобы остальное время работы процессора потратить на масштабирование спрайтов и развитие сюжета игры.

Если создавать звездное небо, привлекая только целые числа как значения скоростей звезд, то едва ли вы заметите количественную ошибку, обусловленную заменой чисел с плавающей запятой на целые.

Конечно, мы могли бы принять компромиссный вариант и использовать числа с фиксированной запятой, но в данном случае овчинка выделки не стоит. Я считаю, что при достаточно высоких скоростях звезд потеря дробной части числа несущественна. Наверное, я уже утомил вас, повторяя одно и то же: в компьютерных играх все и всегда должно быть реализовано самым простым и  быстрым путем. С другой стороны, такое примитивное на первый взгляд оформление игры, как экран, усыпанный звездами, может отнять у программиста удивительно много времени и сил. Но не забывайте, что отличное оформление - залог того, что игрок погрузится в игру с головой!

Листинг 8.3 содержит полный текст программы, позволяющей перемещать космический корабль по трехмерному звездному небу (отметим, что сам звездолет находится в плоскости X-Z). Для перемещения корабля используется вспомогательная цифровая клавиатура. Клавиши Left и Right вращают корабль, а клавиши Up и Down замедляют и ускоряют его движение. Для выхода из .программы нажмите клавишу Q.

При компоновке исполняемого файла вы должны объединить модуль FINVYREN.C с графической библиотекой GRAPHICS.С.

Листинг 8.3. Последний Шаттл (FINVYREN.C).

// ВКЛЮЧАЕМЫЕ ФАЙЛЫ//////////////////////////////

#include <io.h>

#include <conio.h>

#include <stdio.h>

#include <stdlib.h>

#include <dos.h>

#include <bios.h>






#include <fcntl.h>

#include <memory.h>

#tinclude <malloc.h>

#include <math.h>

#include <string.h>

#include <graph.h>

#include "grapics.h" // включаем нашу графическую библиотеку

// прототипы /////////////////////////////////////

void Create_Scale_Data_X(int scale, int far *row) ;

void Create_Scale_Data_Y(int scale, int *row);

void Build_Scale_Table(void);

void Scale_Sprite(sprite_ptr sprite,int scale);

void Clear_Double_Buffer(void);

void Timer(int clicks);

void Init_Stars(void) ;

void Move_Stars(void);

void Draw_Stars(void) ;

// ОПРЕДЕЛЕНИЯ /////////////////////////////////////////////

#define NUM_STARS      50       // количество звезд на небе

#define MAX_SCALE      200      // максимальный размер спрайта

#define SPRITE_X_SIZE  80       // размеры текстуры спрайта

#define SPRITE_y_SIZE  48

// СТРУКТУРЫ /////////////////////////////////////////////// // это звезда

typedef struct star_typ

{

int x,y;        // позиция звезды

int xv,yv;      // скорость звезды

int xa,ya;      // ускорение звезды

int color;      // цвет звезды

int clock;      // число "тиков", которое звезда существует

int acc_time;   // количество "тиков" до ускорения звезды

int acc_count;  // счетчик ускорения

} star, *star_ptr;

// ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ ///////////////////////////////////

unsigned int far *clock = (unsigned int far *)0х0000046CL;

// указатель на внутренний таймер 18.2 "тик"/с

sprite object;

// обобщенный спрайт, который содержит кадры корабля

pcx_picture text_cells;                // файл PCX с кадрами корабля

int *scale_table_y[MAX_SCALE+l] ;

// таблица предварительно рассчитанных коэффициентов масштабирования

int far *scale_table_x[MAX_SCALE+l];

// таблица предварительно рассчитанных коэффициентов масштабирования

star star_field[NUM_STARSj;            // звездное небо

// ФУНКЦИИ //////////////////////////////

void Timer(int clicks)

{ // эта функция использует внутренний таймер с частотой 18.2 "тик"/с



// 32- битовое значение этого таймера находится по адресу 0000:046Ch

unsigned int now;

// получаем текущее время

now = *clock;

// Ожидаем до истечения указанного периода времени.

// Заметьте, что каждый "тик" имеет длительность примерно 55 мс.

while(abs(*clock - now) < clicks){}

} // конец Timer

////////////////////////////////////

void Init_Stars(void)

{

// эта функция инициализирует структуру данных звезд

// при старте программы

int index,divisor;

for (index=0; index<NUM_STARS; index++)

{

star_field[index].x      = 150 + rand() % 20;

star_field[indexl.у      = 90 + rand() % 20;

if (rand()%2==1)

star_field[index].xv     = -4 + -2 * rand() % 3;

else

star_field[index].xv     = 4 + 2 * randO % 3;

if (rand()%2==1)

star_field[index].yv     = -4 + -2 * rand() % 3;

else

star_field[index].yv     = 4 + 2 * randO % 3;

divisor = 1 + rand()%3;

star_field[index].xa    = star_field[index].xv/divisor;

star_field[index] .ya     = star_field [index] .yv/divisor;

star_field[index].color     = 7;

star_field[index].clock     = 0;

star_field[index].acc_time  = 1 + rand() % 3;

star_field[index].acc_count = 0;

} // конец цикла

} // конец Init Stars

////////////////////////////////////////////////////////////

void Move_Stars(void) {

// Эта функция перемещает звезды и проверяет, не вышла ли звезда

// за пределы экрана. Если да, звезда создается вновь.

int index,divisor;

for (index=0; index<NUM STARS; index++)

{

star_field[index].x += star_field[index].xv;

star field[index].y += star_field[index].yv;

// проверка выхода звезды за пределы экрана

if(star_field[index].x>=SCREEN_WJDTH || star_field[index].x<0 ||

star_field[index].y>=SCREEN_HEIGHT || star_field[index].y<0)

{

// восстановление звезды

star_field[index].x      = 150 + rand() % 20;

star_field[index].у      = 90  + randf) % 20;

if (rand()%2==l)

star_field[index].xv     = -4 + -2 * rand() % 3;

else

star_field[index] .xv     = 4 + 2 * rand() % 3;

if (rand()%2==l)



star_field[index].yv     = -4 + -2 * rand() % 3;

else

star_field[index].yv     = 4 + 2 * rand() % 3;

divisor = 1 + rand()%3;

star_field[index].xa     = star_field[index].xv/divisor;

star_field [index] .ya     = star_field.[index] .yv/divisor;

star_field[index].color     = 7;

star_field[index].clock     = 0;

star_field[index].acc_time  = 1 + rand() % 3;

star field[index].ace_count = 0;

} // конец оператора if

// не пора ли ускорить движение звезды

if (++star_field[index].acc_count==star_field[index].асc_time)

{

// обнуляем счетчик

star_field[indexl.acc_count=0;

// ускоряем

star_field[index].xv += star field[index].xa;

star_field[index].yv += star_field[index].ya;

} // конец оператора if

//не пора ли изменить цвет звезды

if (++star_field[index].clock > 5)

{

star_field[index].color = 8;

} // конец оператора if (> 5)

else

if (star_field[index].clock > 10)

{

star_field[index].color =255;

} // конец оператора if (> 10)

else

if (star_field[index].clock> 25)

{

star_field[index].color = 255;

} // конец оператора if (> 25)

} // конец цикла

} // конец Move_Stars

//////////////////////////////////////////////

void Draw_Stars(void)

{

// эта функция рисует звезды в дублирующем буфере

int index;

for (index=0; index<NUM_STARS; index++)

{

Plot_Pixel_Fast_D(star_field[index].х,star_field[index].y, (unsigned char)star_field[index].color) ;

} // конец

цикла

} // конец Draw_Stars

////////////////////////////////////////////////////////////

void Create_Scale_Data_X (int scale, int far *row)

{

// эта функция масштабирует полосу текстуры всех возможных

// размеров и создает огромную таблицу соответствий

int х;

float x_scale_index=0, х_scale_step;

// рассчитываем шаг масштабирования или число исходных пикселей

// для отображения на результирующее изображение за цикл

x_scale_step = (float) (sprite_width)/(float)scale;

x_scale_index+=x_scale_step;

for (x=0; x<scale; x++) {

// помещаем данные в массив для последующего использования



row[x] = (int) (x_scale_index+.5);

if (row[x] > (SPRITE_X_SIZE-1)) row[x] = (SPRITE_X_SI2E-1);

// рассчитываем следующий индекс

х_scale_index+=x_scale_step;

} // конец

цикла

} // конец Create Scale_Data X

////////////////////////////////////////////////////////////

void Create_Scale_Data_Y(int scale, int *row)

{

// эта функция масштабирует полосу текстуры всех возможных

// размеров и создает огромную таблицу соответствий

int у;

float y_scale_index=0, y_scale step;

// рассчитываем шаг масштабирования или число исходных пикселей

// для отображения на результирующее изображение за цикл

y_scale_step = (float)(sprite_height)/(float)scale;

y_scale_index+=y_scale_step;

for (у=0; y<scale; y++)

{ // помещаем данные в массив для последующего использования

row[y] = ((int)(y_scale_index+.5)) * SPRITE_X_SIZE;

if (row[y] > (SPRITE_Y_SIZE-1)*SPRITE_X_SIZE) row[y]] = (SPRITE_Y_SIZE-1)*SPRITE_X_SIZE;

// рассчитываем следующий индекс

y_scale_index+=y_scale_step;

} // конец

цикла

} // конец Create_Scale_Data_Y ////////////////////////////////////////

void Build_Scale_Table(void)

{

// эта функция строит таблицу масштабирования путем расчета

// коэффициентов масштабирования для всех возможных размеров

// от 1 до 200 пикселей

int scale;

// резервируем

память

for (scale=1; scale<=MAX_SCALE; scale++)

{

scale_table_y[scale] = (int *)malloc(scale*sizeof(int)+1);

scale_table_x[scale] = (int far *)_fmalloc(scale*sizeof(int)+1);

} // конец цикла

// создаем Таблицу масштабирования для осей Х и У

for (scale=l; scale<=MAX_SCALE; scale++)

{

// рассчитываем коэффициент для данного масштаба

Create_Scale_Data_Y (scale, (int *)scale_table_y[scale]);

Create_Scale_Data_X (scale, (int far *)scale_table x[scale]);

} // конец

цикла

} // конец Build_Scale_Table

////////////////////////////////////////////////////////////

void Scale_Sprite(sprite_ptr sprite,int scale)

{

// эта функция масштабирует спрайт (без отсечения). Масштабирование

// производится с использованием заранее рассчитанной таблицы,



// которая определяет, как будет изменяться каждый вертикальный

// столбец. Затем другая таблица используется для учета

// масштабирования этих столбцов по оси Х

char far *work_sprite; // текстура

спрайта

int *row_y; // указатель на масштабированные

// по оси У данные (заметьте, что

// это ближний указатель)

int far *row_x;.        // указатель на масштабированные

// по оси Х данные (заметьте, что

// это дальний указатель)

unsigned char pixel;    // текущий

текстель

int x,                  // рабочие переменные

y,

column, work_offset, video_offset, video_start;

// если объект слишком мал, то и рисовать его не стоит

if (scale<l) return;

// рассчитываем необходимые для масштабирования данные

row_y = scale_table_y[scale];

row_x = scale_table_x[scale];

// выбираем соответствующий кадр спрайта

work_sprite = sprite->frames[sprite->curr_frame];

// рассчитываем начальное смещение

video_start

= (sprite->y << 8) + (sprite->y << 6) + sprite->x;

// изображение рисуется слева направо и сверху вниз

for (х=0; x<scale; x++)

{

// пересчитываем адрес следующего столбца

video_offset = video_start + x;

// определяем, какой столбец должен быть отображен,

// исходя из индекса масштабирования по оси Х

column = row_x[x];

// Наконец рисуем столбец обычным образом

for (y=0; y<scale; у++)

{

// проверка на "прозрачность"

pixel = work_sprite[work_offset+column];

if (pixel)

double_buffer[video_offset] = pixel;

// индекс следующей строки экрана и смещение в

// области хранения текстуры

video_offset

+= SCREENJHDTH;

work_offset = row_y[y];

} // конец цикла по Y

} // конец цикла по Х

} // конец Scale_Sprite

////////////////////////////////////////////////////////////

void Clear_Double_Buffer(void)

{

// угадали что это?

_fmemset(double_buffer, 0, SCREEN_WIDTH * SCREEN_HEIGHT + 1) ;

} // конец Clear_Double_Buffer

// ОСНОВНАЯ ПРОГРАММА //////////////////////////////////////

void main(void)

{

// корабль помещается в космическое пространство и игрок



// может перемещать его среди звезд

int done=0,                   // флаг

выхода

scale=64,

direction=6;              // направление корабля (текущий кадр)

float sсale_distance = 24000,

view distance

= 256,

// произвольные константы для согласования масштабирования

// плоской текстуры в трассированном пространстве

х=0,                    // позиция корабля в пространстве

у=0,

z=1024,

xv=0,zv=0,              // скорость корабля в плоскости Х-Z

angle=180,

// угол поворота корабля

ship_speed=10;          // величина скорости корабля

// установка видеорежима 320х200х256

_setvideomode(_MRES256COLOR) ;

// все спрайты будут иметь этот размер

sprite_width = 80;

sprite_height = 48;

// создание таблицы соответствия для подсистемы масштабирования

Build_Scale_Table();

// инициализация

файла PCX, который

содержит все

кадры

PCX_Init((pcx_picture_ptr)&text_cells);

// загрузка файла PCX, который содержит все кадры

PCX_Load("vyrentxt.pcx", (pcx_picture_ptr)&text_cells,1) ;

// резервирование памяти для дублирующего буфера

Init_Double_Buffer();

// инициализация звездного неба

Init_Stars() ;

// установка направления и скорости корабля

angle=direction*30+90;

xv = (float)(ship_speed*cos(3.14159*angle/180));

zv = (float)(ship_speed*sin(3.14159*angle/180));

Sprite_Init((sprite_ptr)&object,0,0,0,0,0,0);

// загрузка 12 кадров космического корабля

PCX_Grap_Bitmap((pcx_picture_ptr)&text_cells, (sprite_ptr)&object,0,0,0) ;

PCX_Grap_Bitmap((pcx_picture_ptr)&text_cells, (sprite_ptr)&object, 1, 1, 0);

PCX_Grap_Bitmap((pcx_picture_ptr)&text_cells,

(sprite_ptr)&object,2,2, 0) ;

PCX_Grap_Bitmap ((pcx_picture_ptr) &text_cells,

(sprite_ptr)Sobject,3,0,1) ;

PCX_Grap_Bitmap((pcx_picture_ptr)&text cells,

(sprite_ptr)&object,4,1,1);

PCX_Grap_Bitmap((pcx_picture_ptr)&text cells,

(sprite_ptr)&object,5,2,1) ;

PCX_Grap_Bitmap ((pcx_picture_ptr) &text_cells,

(sprite_ptr)&object,6,0,2) ;



PCX_Grap_Bitmap ((pcx_picture_pfcr) stext_cells,

(sprite_ptr)&object,7,1,2);

PCX_Grap_Bitmap((pcx picture ptr)&text cells,

(sprite_ptr)Sobject,8,2,2);

PCX_Grap_Bitmap((pcx_picture_ptr)&text_cells,

(sprite_ptr)&object,9,0,3);

PCX_Grap_Bitmap((pcx_picture_ptr)&text_dells,

(sprite_ptr)&object,10,1,3) ;

PCX_Grap_Bitmap ((pcx_picture_ptr) &text_cells,

(sprite_ptr)&object,11,2,3);

// инициализация положения корабля

object.curr_frame = 0;

object.x          = 0;

object.у         =0;

Clear_Double_Buffer() ;

// отслеживание действий игрока и прорисовка корабля

while(!done) {

// нажал ли игрок клавишу?

if (kbhit()) {

switch(getch()) {

case '4' // повернуть корабль влево

{

if (++direction==12)

{

direction==0;

} // конец оператора if

} break;

case'6': // повернуть корабль вправо

{

if (--direction < 0)

{

direction=11;

} // конец оператора if

} break;

case '8' : // ускорить корабль

{

if (++ship_speed > 20) ship_speed=20;

} break; case

'2': // замедлить корабль

{

if (--ship_speed < 0)

ship_speed=0;

} break;

case 'q': // выход из программы

{

done=l;

} break;

default: break;

} // конец

оператора

switch

// векторы направления и скорости

angle=direction*30+90;

xv = (float)(ship_speed*cos(3.14159*angle/180));

zv = (float)(ship_speed*sin(3.14159*angle/180)};

} // конец оператора if

// переместить корабль

x+=xv;

z+=zv;

// ограничить по ближней плоскости

if (z<256)

Z=256;

// рассчитать размер изображения

scale = (int)( scale_distance/z );

// на основании размера изображения

// рассчитать проекции Х и Y

object.х = (int) ((float)x*view_distance / (float)z) + 160 - (scale>>1);

object.у = 100 - (((int) ( (float) y*view_distance / (float)z) + (scale>>1)) );

// ограничить рамками экрана if

(object.х < 0)

object.х

= 0;

else if (object.x+scale >= SCREEN_WIDTH)

object.x = SCREEN_WIDTH-scale;

if (object.у < 0) object.у

= 0;

else

if (object.y+scale >= SCREEN_HEIGHT)



object.у

= SCREEN_HEIGHT-scale;

// выбрать

нужный кадр 

object.curr_frame = direction;

// очистить дублирующий буфер

Clear_Double_Buffer() ;

Move_Stars();

Draw_Stars();

// масштабировать спрайт до нужного размера

Scale_Sprite((sprite_ptr)&object,scale) ;

Show_Double_Buffer(double_buffer);

// вывести на экран информацию для игрока

_settextposition(23,0) ;

printf("Position=(%4.2f,%4.2f,%4.2f)   ",x,y,z) ;

// немного подождать

Timer(1) ;

} // конец while

// удалить файл

PCX PCX_Delete((pcx_picture_ptr)&text_cells) ;

// восстановить текстовый режим

_setvideomode(_DEFAULTMODE);

} // конец функции main

Как и прежде, в теле самой программы располагаются только вызовы функций. Это сделано для облегчения понимания работы алгоритма. При написании реальной, а не демонстрационной программы вам не обязательно поступать так же. Если вы сделаете свою программу чересчур модульной, то может оказаться, что время, потраченное на вызов некоторых функций, станет больше времени их выполнения!

Теперь, после того, как мы узнали математику и механику вывода на экран  преобразований трехмерных спрайтов, давайте посмотрим, как я создавал артинки для программ этой главы.


Содержание раздела