Программный код игры на языке С

Ниже представлен программный код, реализующий игру на языке С.

1. Внимательно изучите листинг программы и комментарий к нему.

2. Запустите программу на выполнение в среде Microsoft Visual C++.

Учтите, что при компиляции программы необходимо изменить конфигурацию проекта таким образом, чтобы при компиляции использовалась многопоточная версия библиотеки С++. Настройка для VC 7 представлена на рисунке.

Также следует учесть, что при запуске консоль полностью не помещается на экране, и поэтому необходимо изменить ее размеры по умолчанию. Для чего необходимо вызвать контекстное меню заголовка окна, выбрать пункт «Умолчания» и установить требуемый размер буфера экрана.

#include "stdafx.h"

#include <windows.h>

#include <process.h>

#include <stdlib.h>

#include <time.h>

#include <stdio.h>

// Объекты синхронизации

HANDLE screenlock; // изменением экрана занимается только один поток

HANDLE bulletsem; // можно выстрелить только три раза подряд

HANDLE startevt; // игра начинается с нажатием клавиши "влево" или "вправо"

HANDLE conin, conout; // дескрипторы консоли

HANDLE mainthread; // Основной поток main

CRITICAL_SECTION gameover;

CONSOLE_SCREEN_BUFFER_INFO info; // информация о консоли

// количество попаданий и промахов

long hit=0;

long miss=0;

char badchar[]="-\\|/";

// Создание случайного числа от n0 до n1

int random(int n0, int n1)

{

if (n0==0&&n1==1) return rand()%2;

return rand()%(n1-n0)+n0;

}

// вывести на экран символ в позицию х и у

void writeat (int x,int y, wchar_t c)

{

// Блокировать вывод на экран при помощи мьютекса

WaitForSingleObject(screenlock,INFINITE);

COORD pos={x,y};

DWORD res;

WriteConsoleOutputCharacter(conout, &c, 1, pos, &res);

ReleaseMutex(screenlock);

}

// Получить нажатие на клавишу (счетчик повторейний в ct)

int getakey(int &ct)

{

INPUT_RECORD input;

DWORD res;

while (1)

{

ReadConsoleInput (conin, &input, 1, &res);

// игнорировать другие события

if (input.EventType!=KEY_EVENT) continue;

// игнорировать события отпускания клавиш

// нас интересуют только нажатия

if (!input.Event.KeyEvent.bKeyDown) continue;

ct=input.Event.KeyEvent.wRepeatCount;

return input.Event.KeyEvent.wVirtualKeyCode;

}

}

// Определить символ в заданной позиции экрана

int getat(int x, int y)

{

wchar_t c;

DWORD res;

COORD org={x,y};

// Блокировать доступ к консоли до тех пор, пока процедура не будет выполнена

WaitForSingleObject(screenlock,INFINITE);

ReadConsoleOutputCharacter(conout,&c,1,org,&res);

ReleaseMutex(screenlock); // unlock

return c;

}

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

void score(void)

{

wchar_t s[128];

swprintf(s, L"Война потоков - Попаданий:%d, Промахов:%d", hit, miss);

SetConsoleTitle(s);

if (miss>=30)

{

EnterCriticalSection(&gameover);

SuspendThread(mainthread); // stop main thread

MessageBox(NULL,L"Игра окончена!",L"Thread War", MB_OK|MB_SETFOREGROUND);

exit(0); // не выходит из критической секции

}

}

// это поток противника

void badguy(void *_y)

{

int y=(int)_y; // случайная координата у

int dir;

int x;

// нечетные у появляются слева, четные у появляются справа

x=y%2?0:info.dwSize.X;

// установить направление в зависимости от начальной позиции

dir=x?-1:1;

// пока противник находится в пределах экрана

while ((dir==1&&x!=info.dwSize.X)||(dir==-1&&x!=0))

{

int dly;

BOOL hitme=FALSE;

writeat(x,y,badchar[x%4]);

for (int i=0;i<15;i++)

{

Sleep(40);

if (getat(x,y)=='*')

{

hitme=TRUE;

break;

}

}

writeat(x,y, ' ');

if (hitme)

{

// в противника попали!

MessageBeep(-1);

InterlockedIncrement(&hit);

score();

_endthread();

}

x+=dir;

}

// противник убежал!

InterlockedIncrement(&miss);

score();

}

// этот поток занимается созданием потоков противников

void badguys(void *)

{

// ждем сигнала к началу игры в течение 15 секунд

WaitForSingleObject(startevt, 15000);

// создаем случайного врага

// каждые 5 секунд появляется шанс создать

// противника с координатами от 1 до 10

while (1)

{

if (random(0,100)<(hit+miss)/25+20)

// со временем вероятность увеличивается

_beginthread(badguy,0, (void *)(random(1,10)));

Sleep(1000); // каждую секунду

}

}

// Это поток пули, каждая пуля - это отдельный поток

void bullet(void *_xy_)

{

COORD xy=*(COORD *)_xy_;

if (getat(xy.X,xy.Y)=='*') return; // здесь уже есть пуля

// надо подождать

// Проверить семафор

// если семафор равен 0, выстрела не происходит

if (WaitForSingleObject(bulletsem,0)==WAIT_TIMEOUT) return;

while (--xy.Y)

{

writeat(xy.X,xy.Y,'*'); // отобразить пулю

Sleep(100);

writeat(xy.X, xy.Y, ' '); // стереть пулю

}

// выстрел сделан.- добавить 1 к семафору

ReleaseSemaphore (bulletsem, 1, NULL);

}

// Основная программа

int _tmain(int argc, _TCHAR* argv[])

{

HANDLE me;

// Настройка глобальных переменных

conin=GetStdHandle(STD_INPUT_HANDLE);

conout=GetStdHandle(STD_OUTPUT_HANDLE);

SetConsoleMode(conin, ENABLE_WINDOW_INPUT);

me=GetCurrentThread(); // не является реальным дескриптором

// изменить псевдодескриптор на реальный дескриптор текущего потока

DuplicateHandle(GetCurrentProcess(),me,GetCurrentProcess(), &mainthread, 0, FALSE, DUPLICATE_SAME_ACCESS);

startevt=CreateEvent(NULL, TRUE, FALSE, NULL);

screenlock=CreateMutex(NULL, FALSE, NULL);

InitializeCriticalSection (&gameover);

bulletsem=CreateSemaphore(NULL, 3, 3, NULL);

GetConsoleScreenBufferInfo(conout, &info);

// Инициализировать отображение информации об очках

score();

// Настроить генератор случайных чисел

srand((unsigned)time(NULL));

// установка начальной позиции пушки

int y=info.dwSize.Y-1;

int x=info.dwSize.X/2;

// запустить поток badguys; ничего не делать до тех пор,

// пока не произойдет событие или истекут 15 секунд

_beginthread (badguys, 0, NULL); // основной цикл игры

while (1)

{

int c,ct;

writeat(x,y, '|'); // нарисовать пушку

c=getakey(ct); // получить символ

switch (c)

{

case VK_SPACE:

static COORD xy;

xy.X=x;

xy.Y=y;

_beginthread(bullet,0,(void *)&xy);

Sleep(100); // дать пуле время улететь на некоторое расстояние

break;

case VK_LEFT: // команда "влево!"

SetEvent(startevt); // поток badguys работает

writeat(x,y,' '); // убрать с экрана пушку

while (ct--) // переместиться

if (x) x--;

break;

case VK_RIGHT: // команда "вправо!"; логика та же

SetEvent(startevt);

writeat(x,y,' ');

while (ct--)

if (x!=info.dwSize.X-1) x++;

break;

}

}

}

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

Программа использует несколько синхронизационных объектов. Мьютекс screenlock предотвращает одновременный доступ нескольких потоков к экрану консоли. Семафор bulletsem ограничивает количество пуль, одновременно находящихся. Событие startevt блокирует генерацию врагов до тех пор, пока пользователь не нажмет на клавишу или не истечет 15 секунд. Критическая секция gameover предотвращает многократный вывод на экран сообщения об окончании игры.

В начале работы функция main производит инициализацию множества объектов, включая объекты синхронизации. Дескриптор потока сохраняется таким образом, что когда игра завершается, подпрограмма score блокирует обработку ввода. Следует обратить внимание на то, что дескриптор, возвращаемый функцией GetCurrentThreadHandle, на самом деле не является реальным дескриптором. Это специальное значение, которое обозначает текущий поток. Например, если сохранить это значение, а потом передать его вызову SuspendThread, вы автоматически приостановите выполнение потока, который выполнил этот вызов. Чтобы получить реальный дескриптор, функция main обращается к вызову DuplicateHandle.

Прежде чем функция main войдет в цикл обработки ввода, она запускает функцию badguys в новом потоке. Функция badguys ожидает событие startevt в течение 15 секунд. Если событие происходит или 15 секунд истекают, начинается генерация врагов. Ожидание реализуется при помощи функции WaitForSingleObject,

Выполнив начальную инициализацию, функция main входит в цикл ожидания клавиатурного ввода и вывода на экран консоли изображения пушки. Нажатия на клавиши «влево» и «вправо» изменяют положение пушки, а при нажатии на клавишу «пробел» создается новый программный поток, в котором запускается функция bullet.

Поток badguys каждую секунду с некоторой вероятностью создает нового противника. Для этого он проверяет случайно полученное значение, после чего ждет в течение секунды и вновь проверяет случайное значение. Если он решает, что необходимо создать нового противника, он случайным образом определяет y координату нового врага и запускает поток badguy.

Если сравнивать с другими потоками программы, поток badguy выполняет несколько более сложные действия. Прежде всего, он определяет, будет ли противник двигаться справа налево или слева направо, а затем начинает перемещение противника. По мере того как он перемещает противника, он постоянно проверяет, произошло ли столкновение с пулей. Если столкновение произошло, поток отмечает попадание и обращается к функции score для того, чтобы обновить ин­формацию об очках. Если противник скрывается за границей экрана, поток также отмечает это и вновь обращается к score. Функция score не только обновляет заголовок окна, но также проверяет условие окончания игры.

Для обновления переменных hit и miss, в которых хранится количество попаданий и промахов, используется вызов InterlockedIncrement. При этом, если два врага одновременно попытаются изменить значения этих переменных, все сработает нормально.

Если срабатывает условие завершения игры, функция score выполняет критическую секцию gameover. Таким образом, сообщение об окончании игры появится на экране только один раз. При этом также происходит завершение работы потока main и обращение к exit для того, чтобы завершить игру. Для этого пользователь должен подтвердить завершение игры.

Поток bullet проверяет, можно ли осуществить выстрел. Этот поток немедленно завершает работу в случае, если в позиции, куда должна переместиться пуля, уже находится другая пуля. Это может произойти в случае, если пользователь стреляет слишком быстро. Поток bullet также производит проверку семафора bulletsern. Чтобы выстрел произошел, значение семафора должно быть отличным от нуля. Благодаря семафору на экране не могут одновременно находиться три пули. Если поток bullet не может завладеть семафором, он завершает работу. Таким образом, создать четвертую пулю нельзя – игра просто игнорирует запрос.


Понравилась статья? Добавь ее в закладку (CTRL+D) и не забудь поделиться с друзьями:  



double arrow
Сейчас читают про: