Система координат канви
Система координат канви
Вступ
Розділ 1. Теоретична частина
1.1 Компонент Image і деякі його
властивості
1.2 Вивід зображень за допомогою
пікселів
1.3 Збереження конфігурації в файлах
.ini
Розділ 2. Практична частина
2.1 Код гри
2.2 Опис гри
Висновок
Використана література
Додатки
Вступ
Багато компонентів в C++Builder мають
властивість Canvas (канва, полотно), є областю компоненту, на якій можна
малювати або відображати готові зображення. Цю властивість мають форми,
графічні компоненти Image, PaintBox, Bitmap і багато інших. Канва містить
властивості і методи, що істотно спрощують графіку C++Builder. Всі складні
взаємодії з системою заховані для користувача, так що малювати в C++Builder
може людина абсолютно не досвідчена в машинній графіці.
Кожна точка канви має координати X і
Y. Система координат канви, як і скрізь в C++Builde, має початком лівий верхній
кут канви. Координата X зростає при переміщенні зліва направо, а координат Y —
при переміщенні зверху вниз.
З координатами ви вже мали справу
багато разів, але поки вас не дуже цікавило, що стоїть за ними, в яких одиницях
вони вимірюються. Координати вимірюються в пікселах. Піксел — це найменший
елемент поверхні малюнка, з яким можна маніпулювати. Найважливіша властивість
піксела — його колір. Для опису кольору використовується тип TColor. З кольором
ви зустрічаєтеся практично в кожному компоненті і знаєте, що в C++Builder
визначена безліч констант типу TColor. Одні з них безпосередньо визначають
кольори (наприклад clBlue — синій), інші визначають кольори елементів вікон,
які можуть мінятися залежно від вибраної користувачем палітри квітів Windows
(наприклад, clBtnFace — колір поверхні кнопок).
Розділ 1.
Теоретична частина
1.1 Компонент Image і деякі його
властивості
Нерідко
виникає потреба прикрасити своє застосування якимись картинками. Це може
бути графічна заставка, що є логотипом вашого застосування. Або це
можуть бути фотографії при розробці застосування, що працює з базою даних
співробітників якоїсь установи. У першому випадку вам буде потрібно компонент
Image, розташований на сторінці Additional бібліотеки компонентів, в
другому — його аналог DBImage, пов'язаний з даними і розташований на сторінці Data Controls.
Почнемо
знайомство з цими компонентами. Відкрийте нове застосування і перенесіть на форму
компонент Image. Його властивість, яка може містити картинку, — Picture.
Натисніть на кнопку з багатокрапкою біля цієї властивості або просто зробіть подвійне клацання на
Image, і перед вами відкриється вікно Picture Editor, що дозволяє завантажити у
властивість Picture який-небудь графічний файл
(кнопка Load), а також зберегти відкритий файл під новим ім'ям або в новому каталозі. Клацніть на Load, щоб завантажити
графічний файл. Перед вами відкриється вікно Load Picture. У міру переміщення
курсора в списку по графічних файлах
в правому вікні відображаються зображення, що містяться в них. Ви можете знайти
графічні файли в каталозі Images. Він зазвичай
розташований в каталозі ...\Program files\Common
Files\Borland Shared.
У вікні
завантаження графічного файлу ви можете не тільки проглянути зображення, що зберігається у
вибираному файлі, але і побачити розмір зображення — цифри в дужках справа вгорі. В деяких випадках, як ви побачите
пізніше, це важливо.
Після
завантаження файлу клацніть на ОК і у вашому компоненті Image відобразиться вибрана вами картинка. Можете
запустити ваше застосування і помилуватися нею. Втім, ви і так побачите
картинку, навіть не виконуючи програма.
Коли ви в процесі проектування завантажили
картинку з файлу в компонент Image, він не просто відображає її, але і зберігає
в застосуванні. Це дає вам можливість поставляти ваше застосування без окремого
графічного файлу. Втім, як ми побачимо пізніше, в Image можна завантажувати і
зовнішні графічні файли в процесі виконання застосування.
Повернемося до розгляду властивостей
компоненту Image.
Якщо встановити властивість AutoSize
в true, то розмір компоненту Image автоматично підганятиметься під розмір
помішаної в нього картинки. Якщо ж властивість AutoSize встановлена в false, то
зображення може не поміститися в компонент або, навпаки, плошадь компоненту
може опинитися багато більше площі зображення.
Інша властивість — Stretch дозволяє
підганяти не компонент під розмір малюнка, а малюнок під розмір компоненту.
Встановіть AutoSize в false, розтягніть або стисніть розмір компоненту Image і
встановіть Stretch в true. Ви побачите, що малюнок займе всю площу компоненту,
але оскільки навряд чи реально встановити розміри Image пропорційними розміру
малюнка, те зображення спотвориться. Встановлювати Stretch в true може мати
сенс тільки для якихось узорів, але не для картинок. Властивість Stretch не діє
на зображення піктограм, які не можуть змінювати своїх розмірів.
Властивість
— Center, встановлене в true, центрує зображення на площі Image, якщо розмір компоненту
більше розміру малюнка.
Розглянемо
еше одна властивість - - Transparent (прозорість). Якщо Transparent рівне
true, то зображення в Image стає прозорим. Це можна використовувати для накладення зображень один
на одного. Помістите на форму другий компонент Image і завантажите в нього іншу
картинку. Тільки постарайтеся узяти яку-небудь малозаполненую, контурну
картинку. Можете, наприклад, узяти картинку
з числа тих, що поміщаються зазвичай на кнопки, наприклад, стрілку (файл ...\program files\common
files\borland shared\images\buttons\arrowlr.bmp). Пересуньте
ваші Image так, щоб вони перекривали один одного, і у верхньому
компоненті встановите Transparent рівним true. Ви побачите, що верхня
картинка перестала затуляти нижнюю. Одне з можливих застосувань цієї
властивості — накладення на картинку написів, виконаних у вигляді бітової матриці. Ці
написи можна зробити за допомогою вбудованої
в C++Builder програми Image Editor, яка буде розглянута пізніше.
Врахуйте,
що властивість Transparent діє тільки на бітові матриці.
Будь-яка картинка, креслення або
схема можуть розглядатися як сукупність графічних примітивів: крапок, ліній,
кіл, дуг і ін. Таким чином, для того, щоб на екрані з'явилася потрібна
картинка, програма повинна забезпечити викреслювання (вивід) графічних
елементів - примітивів, складових цю картинку.
Викреслювання графічних примітивів на
поверхні (форми або компоненту image - області виведення ілюстрації)
здійснюється застосуванням відповідних методів до властивості Canvas цієї
поверхні.
Викреслювання прямої лінії виконує
метод LineTo. Метод малює лінію з тієї точки, в якій в даний момент знаходиться
олівець (ця точка називається поточною позицією олівця або просто "поточною"),
в точку, координати якої вказані в інструкції виклику методу.
Наприклад, оператор
Canvas->LineTo(100,200)
малює лінію в точку з координатами
(100, 200), після чого поточною стає точка з координатами (100, 200).
Початкову точку лінії можна задати, перемістивши
олівець в потрібну точку графічної поверхні. Зробити це можна за допомогою
методу MoveTo, вказавши як параметри координати точки початку лінії. Наприклад,
оператори
Canvas->MoveTo(10,10);
//встановити олівець в точку (10,10)
Canvas->LineTo(50,10);
// лінія з точки (10,10) в точку
(50,10)
малюють горизонтальну лінію з точки
(10, 10) в точку (50, 10).
Використовуючи властивість поточної
точки, можна намалювати ламану лінію. Наприклад, оператори
Canvas->MoveTo(10,10);
Canvas->LineTo(50,10);
Canvas->LineTo(10,20);
Canvas->LineTo(50,20);
малюють лінію, схожу на букву Z.
Виведення тексту (рядків типу
AnsiString) на поверхню графічного об'єкту забезпечує метод TextOutA. Інструкція виклику методу TextOutA в загальному вигляді виглядає
таким чином:
Canvas->TextOutA(x,у,Текст)
Параметр текст задає текст, що
виводиться. Параметри х і у визначають координати точки графічної поверхні, від
якої виконується виведення тексту.
Шрифт, який використовується для
виведення тексту, визначається значенням властивості Font відповідного об'єкту Canvas. Властивістю Font є об'єкт типу
TFont. У табл. 3.4 перераховані властивості об'єкту TFont, що визначають
характеристики шрифту, використовуваного методом TextOutA для виведення тексту.
Name Використовуваний шрифт.
Як значення слід використовувати назву шрифту (наприклад, Arial)
Size Розмір шрифту в пунктах
(points). Пункт - це одиниця вимірювання
розміру шрифту, використовується в поліграфії. Один пункт рівний 1/72 дюйми.
Style Стиль зображення символів.
Можливо: нормальним, напівжирним, курсивним, підкресленим, перекресленим. Стиль
задається за допомогою наступних констант: fsBold (напівжирний), fsltalic
(курсив), fsUnderline (підкреслений), fsStrikeOut (перекреслений). Властивість
Style є множиною, що дозволяє комбінувати необхідні стилі. Наприклад,
інструкція, яка встановлює стиль "напівжирний курсив", виглядає так:
Canvas->Font->Style=TFontStyles() <<fsBold<<fsUnderline
Color Колір символів. Як значення
можна використовувати константу типу TColor
При виведенні тексту вельми корисні
методи TextWidth і TextHeight, значеннями
яких є відповідно ширина і висота області виведення тексту, які, очевидно,
залежать від характеристик використовуваного шрифту. Обом цим методам як
параметр передається рядок, який передбачається вивести на поверхню методом
TextOutA.
Наступний фрагмент коду демонструє
використання методів, що забезпечують виведення тексту на поверхню форми.
Приведена функція обробки події OnPaint закрашує верхню половину вікна білим,
нижню - блакитним кольором, потім в центрі вікна, по межі закрашених областей,
виводить текст.
void fastcall TForml: : ForroPaint (TObject *Sender)
{AnsiString ms = "Borland C++Builder";
TRect aRect;
int x,y; // точка, від якої буде
виведений текст
// верхню половину вікна фарбуємо
білим
aRect =
Rect(0,0,ClientWidth,ClientHeight/2);
Canvas->Brush->Color = clWhite; Canvas->FillRect(aRect);
// нижню половину вікна фарбуємо
блакитним
aRect =
Rect(0,ClientHeight/2,ClientWidth,ClientHeight);
Canvas->Brush->Color = clSkyBlue;
Canvas->FillRect(aRect);
Canvas->Font->Name = "Times New Roman";
Canvas->Font->Size = 24;
// Canvas->Font->Style = TFontStyles() <<fsBold<< fsltalic;
// текст розмістимо в центрі вікна
х = (ClientWidth -
Canvas->TextWidth(ms))/2;
у = ClientHeight/2 - Canvas->TextHeight(ms)/2;
Canvas->Brush->Style = bsClear;
// область виведення тексту
// не закрашувати
Canvas->Font->Color = clBlack;
Canvas->TextOutA(x,y,ms); // вивести текст }
1.2 Вивід зображень за допомогою
пікселів
Малювати на канві можна різними
способами. Перший варіант — малювання по пикселам. Для цього використовується
властивість канви Pixels. Цією властивістю є двовимірний масив
Canvas->Pixels[intX][int Y], який відповідає за кольори канви. Наприклад,
Canvas->PixeIs[10][20] відповідає кольору пиксела, 10-го зліва і 20-го
зверху. З масивом пикселов можна звертатися як з будь-якою властивістю:
змінювати колір, задаючи пикселу нове значення, або визначати його колір по
значенню, що зберігається в нім. Наприклад, Canvas->PixeIs[10][20]= clBlack
— це завдання пикселу чорного кольору.
Давайте спробуємо намалювати графік
деякої функції F(X) наканве компоненту Imagel, якщо відомий діапазон її зміни Ymax
і Ymin і діапазон зміни аргументу Xmin і Хтах. Це можна зробити такою
процедурою:
float X,Y; // координати функції
int PX,PY; // координати пикселов
for (РХ = 0: РХ <= Imagel->Width; Рх++)
//Х- координата, відповідна пикселу з
координатою РХ
X = Xmin + РХ * (Xmax - Xmin) / Imagel->Width;
Y = F(X); //PY - координата пиксела, відповідна
координаті Y
PY = imagel->Height - (Y -
Ymin)*Imagel->Height/(Ymax-Ymin); //Устанавливается чорний колір вибраного пиксела
Imagel->Canvas->Pixels[PX][PY] = clBlack;}
У цьому коді вводяться змінні X і Y,
що є значеннями аргументу і функції, а також змінні РХ і PY, що є координатами
пикселов, відповідними X і Y. Сама процедура складається з циклу по всіх
значеннях горизонтальної координати пикселов РХ компоненту Imagel. Спочатку
вибране значення Рхнересчитиваєтсявсоответствующєєзначенієх. Потім проводиться
виклик функції і визначається її значення Y. Це значення перераховується у
вертикальну координату пиксела PY. І на закінчення колір пиксела з координатами
(РХ, PY) встановлюється чорним.
Спробуйте створити відповідне
застосування і подивитися, як воно працює. Хай для простоти ми
орієнтуватимемося на функцію sin(X), для якої Xmin=0, Хmax=4pi (2 періоди в
радіанах), Ymin=-1, Ymax=l.
Почніть новий проект, помістіть на
нього компонент Image і кнопку з написом «Намалювати», в обробник події OnClick
якою запишіть код, аналогічний приведеному вище, але що конкретизує функцію:
#define Pi 3.14159
float X, Y; // координати функції
int РХ, PY; // координати пикселов
for (РХ = 0: РХ <-
Imagel->Width; PX++)
{//X - координата, відповідна пикселу
з координатою РХ
X = РХ * 4 * Pi / imagel->Width;
Y = sin(X); //PY - координата пиксела,
відповідна координаті У
PY = Imagel->Height - (Y+1) * Imagel->Height
/ 2; //Устанавливается чорний колір вибраного пиксела
Imagel->Canvas->Pixels(PX][PY] = clBlack; }
1.3
Збереження
конфігурації в файлах ini
|Файли .ini - це текстові файли,
призначені для зберігання інформації про настройки
різних програм. Інформація у файлі логічно групується в
розділи, кожен з яких починається оператором заголовка, поміщеним в квадратні дужки. Наприклад [Desktop|].
У рядках, наступних за заголовком, міститься інформація, що відноситься до
даного розділу, у формі:
<ключ>=<значення>
[dBASE| Files|]
Driver32=C|:\WINDOWS\SYSTEM\odbcjt32.dll
Файли .ini, як правило,
зберігаються в каталозі
Windows|, який можна
знайти за допомогою функції
GetWindowsDirectory|.
У C++Builder|
роботу з файлами
.ini найпростіше здійснювати за допомогою створення в програмі об'єкту типу
TIniFile|. Цей
тип описаний в модулі inifiles|, який треба підключати до програми оператором uses|
(автоматично це не робиться).
При створенні об'єкту типу TlniFile| в
нього передається ім'я файлу .ini, з яким він зв'язується.
Файл повинен існувати до створення об'єкту.
Для запису
значень ключів існує багато методів: WriteString|,
WriteInteger|, WriteFloat|, |і ін. Кожен з них записує значення відповідного
типу. Оголошення всіх цих методів дуже схожі. Наприклад:
void| fastcall| WriteString| (const|
AnsiString| Section|
const| AnsiString| Ident|, const| AnsiString| Value|);
void| fastcall| Writelnteger| (const|
AnsiString| Section|
const| AnsiString| Ident|, int| Value|);
У всіх оголошеннях Section| - розділ файлу, Ident| - ключ цього розділу, Value| - значення ключа.
Якщо відповідний розділ
або ключ відсутній
у файлі, він автоматично створюється.
Є аналогічні методи
читання: ReadString|, Readlnteger|,
ReadFloat|, ReadBool| і ін. Наприклад:
AnsiString| fastcall| ReadString| (const| AnsiString| Section|
const| AnsiString| Ident|, const| AnsiString| Default|);
int| fastcall| Readlnteger| (const|
AnsiString| Section|
const| AnsiString| Ident|, int| Default|);
Методи повертають значення ключа|
розділу Section|. Параметр Default| визначає значення, що
повертається у випадку, якщо у файлі не вказано значення відповідного ключа.
Перевірити наявність значення ключа можна методом ValueExists|, в який передаються імена розділу і ключа. Метод
DeleteKey| видаляє з файлу значення вказаного ключа у вказаному розділі. Перевірити наявність у файлі
необхідного розділу можна методом SectionExists|. Метод EraseSection|
видаляє з файлу вказаний розділ разом зі всіма
його ключами. Є ще ряд методів, які ви можете подивитися у
вбудованій довідці C++Builder|.
Подивимося на прикладі, як все це можна використовувати для установки
програми, запам'ятовування її настройоки і для видалення програми.
Зробіть просте тестову програму. Перенесіть на форму три кнопки Button| і діалог FontDialog|.
Перша кнопка (назвіть її BInst| і задайте напис Install|)
імітуватиме установку програми. Точніше, не саму
установку, оскільки копіювати файли з загрузочної дискети
ми не будемо, а тільки створення файлу .ini у каталозі Windows|. Друга кнопка (назвіть
її BUnlnst| і задайте напис Unlnstall|) імітуватиме видалення програми. Тут ми не
видалятимемо саму програму з диска, а тільки видалимо з
каталога Windows| наш файл, а третя кнопка
(назвіть її BFont| і задайте напис Font|) дозволятиме змінювати ім'я шрифту,
використовуваного у формі, і забезпечить запам'ятовування цього шрифту у файлі
.ini, щоб надалі при запуску програми можна було читати цю настройку і
задавати її формі.
Розділ 2.
Практична частина
2.1 Код гри
#include <vcl.h>
#pragma hdrstop
#include
"UBilliard.h"
#include <math.h>
#include
"inifiles.hpp"
#pragma package(smart_init)
#pragma resource
"*.dfm"
// розмір столу
int const w = 600;
int const h = 300;
class TGameStatus {
protected: char* gsStatus;
public:
void gsBegin(){
gsStatus="qsBegin";
}
void gsGame(){
gsStatus="gsGame";
}
void gsGameOver(){
gsStatus="gsGameOver";
}};
TGameStatus *GameStatus =
new TGameStatus;
struct TPlayer{
int balls; };
class TLose{
public:
int x,y;
int ballsInside;
int R;
void Draw();};
class TBall { public:
float x,y, dx,dy;
int R;
bool exists, stopped ;
int col,ID,count;
TList *Items;
void Draw();
void Stop();
bool InLose(int
&Number);
void outFrom(TBall b);
TBall CollisedWith();
~TBall();};
class TCue {
public:
bool Visible;
TBall ToBall;
float Angle, energy;
void draw();
void Hit();};
void TCue::Hit(){
TBall *ToBall;
ToBall->dx
=-cos(Angle)*energy;
ToBall->dy =
-sin(Angle)*energy;
ToBall->stopped = False;
Visible = False;}
class TBilliardTable{
public:
int Width, Height, Left,
Right, Top, Bottom;
TList* Ball;
TList* Lose;
TCue Cue;
void Draw();};
float GetAngToXY( pBall b
;float hitX, hitY){
float dx, dy, d;
dx = b->x - hitX;
dy = b->y - hitY;
d = sqrt(dx*dx+dy*dy);
if(dy>0)
Result = arccos(dx/d);
else Result = -arccos(dx/d);
}
void
TBall::outFrom(pBall:b){
{ AB,
aa, bb,
V1, V2,
aPrXx, aPrXy,
aPrYx, aPrYy,
aPrX, aPrY,
bPrXx, bPrXy,
bPrYx, bPrYy,
bPrX, bPrY,
alfaA, betaA, gammaA,
Extended alfaB, betaB,
gammaB;
if(b == NULL ) exit;
AB = sqrt(sqr(x-b->x)+sqr(y-b->y));
V1 = sqrt(dx*dx+dy*dy);
V2 = sqrt(b->dx*b->dx+b->dy*b->dy);
aPrXx = 0;
aPrXy = 0;
bPrXx = 0;
bPrXy = 0;
////////-ball #1-
if(V1>0 ) {
if((b->y-y)>0
alfaA = arccos((b->x-x)/AB);
else alfaA = -arccos((b->x-x)/AB);
if(dy>0
then gammaA = arccos(dx/V1)
else gammaA = -arccos(dx/V1);
betaA = gammaA-alfaA;
aPrX = V1*cos(betaA);
aPrY = V1*sin(betaA);
aPrXx = aPrX*cos(alfaA);
aPrXy = aPrX*sin(alfaA);
aPrYx = aPrY*sin(alfaA);
aPrYy = aPrY*cos(alfaA); }
//////////////-ball #2-
if(V2>0 ) {
if((y-b->y)>0
then alfaB = arccos((x-b->x)/AB);
//=alfaA+pi
else alfaB = -arccos((x-b->x)/AB);
if(b->dy>0)
then gammaB = arccos(b->dx/V2);
else gammaB = -arccos(b->dx/V2);
betaB = gammaB-alfaB;
bPrX = V2*cos(betaB);
bPrY = V2*sin(betaB);
bPrXx = bPrX*cos(alfaB);
bPrXy = bPrX*sin(alfaB);
bPrYx = bPrY*sin(alfaB);
bPrYy = bPrY*cos(alfaB); }
dx = ((dx - 2*aPrXx) +
bPrXx)*mu; // = mu*(bPrXx - aPrXx)
dy = ((dy - 2*aPrXy) +
bPrXy)*mu; // = mu*(bPrXy - aPrXy)
b->dx = ((b->dx -
2*bPrXx) + aPrXx)*mu; // = mu*(aPrXx - bPrXx)
b->dy = ((b->dy -
2*bPrXy) + aPrXy)*mu; // = mu*(aPrXy - bPrXy)}
void InitSound(){
pcm->wFormatTag = WAVE_FORMAT_PCM;
pcm->nChannels = 1;
pcm->nSamplesPerSec = 44100;
pcm->nAvgBytesPerSec = 2*44100;
pcm->nBlockAlign = 2;
pcm->wBitsPerSample = 16;
pcm->cbSize = 0;
WaveOut = 0;
open_status = waveOutOpen(&WaveOut,
0, &pcm, Form1->Handle,
0, callback_Window)}
float CalCulateAngle(){
{ int i, j;
pLose ToLz, lz;
pBall nearestBall, Bl, b;
float hitX, hitY;
minAng, a2Lz, minD,
float dx, dy, a, minDist, d;
minDist = 1.7e+308;
minD = minDist;
with BilliardTable do
{ for( j = 0; j
<=Lose->Count-1; j ++)
for( i = 0; i
<=Ball->Count-1; i ++)
{ lz = Lose->Items[j];
b = Ball->Items[i];
if(! b->exist ) continue;
d = sqrt(sqr(b->x-lz->x)+sqr(b->y-lz->y));
if(d < minDist )
{ minDist = d;
ToLz = lz;
Bl = b; } }
if((Bl == NULL) ) exit;
dx = Bl->x - ToLz->x;
dy = Bl->y - ToLz->y;
d = sqrt(dx*dx+dy*dy);
if((dy)>0
a2Lz = arccos(dx/d);
else a2Lz = -arccos(dx/d);
hitX = Bl->x +
cos(a2Lz)*Bl->R;
hitY = Bl->y +
sin(a2Lz)*Bl->R;
minAng = 1.7e+308;
for( i = 0; i
<=Ball->Count-1; i ++)
{ b = Ball->Items[i];
if((b->ID == Bl->ID)
|| (not b->exist)
continue;
a = GetAngToXY(b, hitX,
hitY);
if(abs(a2Lz-a) < minAng )
{ minAng = abs(a2Lz-a);
nearestBall = b;
Result = a; } }
for( i := 0 to Ball.Count-1
do
begin
b := Ball.Items[i];
if (b.ID = Bl.ID) or (not
b.exist)
continue;
d :=
sqrt(sqr(b.x-Bl.x)+sqr(b.y-Bl.y));
if d < minD then
begin
minD := d;
nearestBall := b;
end;
end;
dx := Bl.x - ToLz.x;
dy := Bl.y - ToLz.y;
d := sqrt(dx*dx+dy*dy);
if (dy)>0
a2Lz := arccos(dx/d)
else a2Lz := -arccos(dx/d);
hitX := Bl.x +
cos(a2Lz)*Bl.R;
hitY := Bl.y +
sin(a2Lz)*Bl.R;
dx := nearestBall.x - hitX;
dy := nearestBall.y - hitY;
d := sqrt(dx*dx+dy*dy);
if (dy)>0
then a := arccos(dx/d)
else a := -arccos(dx/d);
Result := a;}
void ComputerMove(){
Cue->visible = True;
CompAngle = CalculateAngle;
if(CompAngle >
Cue->angle)
CompMove = 1;
else CompMove = -1;}
void TBilliardTable::Draw(){
{ int i;
pBall *b;
pLose *lz;
char* WhoIsIt;
Canvas->Brush->Color =
clBlack;
Canvas->Pen->Color = clBlack;
Canvas->Rectangle(0,0,Width,
Height);
Canvas->Brush->Color =
$336699;
Canvas->Pen->Color = clYellow;
Canvas->Rectangle(BilliardTable->Left
- LoseSize, BilliardTable->Top - LoseSize,
BilliardTable->Right +
LoseSize, BilliardTable->Bottom + LoseSize);
Canvas->Brush->Color =
clGreen;
Canvas->Rectangle(BilliardTable->Left,
BilliardTable->Top, BilliardTable->Right, BilliardTable->Bottom);
Canvas->Pen->Color = clYellow;
Canvas->Pen->Color = clBlack;
Canvas->Ellipse(BilliardTable->Left
+ (3 * BilliardTable->Width / 4)-2,
BilliardTable->Top +
(BilliardTable->Height / 2)-2,
BilliardTable->Left + (3
* BilliardTable->Width / 4)+2,
BilliardTable->Top +
(BilliardTable->Height / 2)+2);
Canvas->Brush->Color =
$336699;
Canvas->Font->Color = clYellow;
Canvas->Font->Style = [];
if(Player == 0
then WhoIsIt = "Игрок";
if(Player == 1
then WhoIsIt = "Компьютер";
Canvas->TextOut(BilliardTable->Left+30,
dh + 1,
"Ход:
"+WhoIsIt+"а");
Canvas->TextOut(BilliardTable->Left+30,
BilliardTable->Bottom,
"В
лузах:"+IntToStr(InLoses));
if(Player == 0 )
Canvas->Font->Style = [fsBold];
Canvas->TextOut(BilliardTable->Right-150,
BilliardTable->Bottom,
"Игрок:"+IntToStr(PlayerN[0]->balls));
Canvas->Font->Style = [];
if(Player == 1 )
Canvas->Font->Style = [fsBold];
Canvas->TextOut(BilliardTable->Right-150,
dh + 1,
"Компьютер:"+IntToStr(PlayerN[1]->balls));
Canvas->Brush->Color =
clBlack;
Canvas->Font->Color = clYellow;
Canvas->Font->Style = [fsBold];
Canvas->Font->Style = [];
for( i = 0; i
<=Lose->Count-1; i ++)
{ lz = Lose->Items[i];
lz->Draw; }
for( i = 0; i
<=Ball->Count-1; i ++)
{ b = Ball->Items[i];
if(b->exist ) b->Draw;
}
if(Cue->visible )
Cue->Draw; }
gsGameOver:
{ }
case } // case;break;; }
void TLose::Draw()
{ with
Form1->Image1->Canvas do
{ Brush->Color = clBlack;
Pen->Color = clYellow;
Ellipse(Trunc(x-r),
Trunc(y-r),
Trunc(x+r), Trunc(y+r));
Font->Color = clWhite;
TextOut(x-4,y-8,
IntToStr(ballsInside)); } }
void TBall::Draw()
{ with
Form1->Image1->Canvas do
{ Brush->Color = col;
Pen->Color = col;//clBlack;
Ellipse(Trunc(x-r),
Trunc(y-r),
Trunc(x+r), Trunc(y+r));
Brush->Color = clWhite;
Pen->Color = clWhite;
Ellipse(Trunc(x-r*sqrt(2)/2*0.5-2),
Trunc(y-r*sqrt(2)/2*0.5-2),
Trunc(x-r*sqrt(2)/2*0.5+2),
Trunc(y-r*sqrt(2)/2*0.5+2));
Brush->Color = col;
Font->Color = clWhite -
col;
if(ShowID )
{ TextOut(Trunc(x-4),
Trunc(y-8), IntToStr(ID)); }
Refresh; } }
void TCue->Draw()
{ int x1, y1, x2, y2, x3,
y3;
Brush->Color = clYellow;
Pen->Color = clYellow;
Pen->Width = 4;
x1 = Trunc(ToBall->x+cos(Angle)*(ToBall->R+energy));
y1 = Trunc(ToBall->y+sin(Angle)*(ToBall->R+energy));
x2 = Trunc(ToBall->x+cos(Angle)*(ToBall->R+CueLength+energy));
y2 = Trunc(ToBall->y+sin(Angle)*(ToBall->R+CueLength+energy));
x3 = Trunc(ToBall->x-cos(Angle)*1000);
y3 = Trunc(ToBall->y-sin(Angle)*1000);
MoveTo(x1, y1);
LineTo(x2, y2);
Pen->Width = 1;
Brush->Color = clWhite;
Pen->Color = clWhite;
Ellipse(x1-2,y1-2,x1+2,y1+2);
if(ShowLine )
{ Pen->Style = psDash;
MoveTo(x1, y1);
LineTo(x3, y3);
Pen->Style = psSolid; } }
}
void TBall->Stop;
{ dx = 0;
dy = 0;
stopped = true; }
bool TBall::InLose(){
int Number;
int i;
pLose lz;
boolean inLz;
Result = False;
if(! exist ) exit;
for( i = 0; i
<=BilliardTable->Lose->Count-1; i ++)
{ lz = BilliardTable->Lose->Items[i];
inLz = sqrt(sqr(x-lz->x)+sqr(y-lz->y))
<== lz->R;
if(inLz )
{ Number = i;
Result = True;
inc(InLoses);
Exit; } } }
pBall
TBall->CollisedWith();
{ int j;
pBall bb;
real d, delta, ddx, ddy;
Result = NULL;
if(! exist ) exit;
for( j = 0; j
<=BilliardTable->Ball->Count-1; j ++)
{ bb = BilliardTable->Ball->Items[j];
if(! bb->exist )
continue;
if(bb->ID == ID )
continue;
d = sqrt(sqr(x-bb->x)+sqr(y-bb->y));
if((d <== R + bb->R) )
{ delta = (R+bb->R - d)/2
+ 1;
ddx = (bb->x-x)/d;
ddy = (bb->y-y)/d;
x = x - ddx*delta;
y = y - ddy*delta;
bb->x = bb->x +
ddx*delta;
bb->y = bb->y +
ddy*delta;
Result = bb;
exit; } } }
//initial
int ballSize = 10; //розмір
куль
int loseSize = ballSize + 5;
int MaxEnergy= 20; // сила
максимального удару
int CueLength = 200;
//довжина кия
float mu = 0.97;
float Step = 0.03; //
переміщення
int PyramidHeight;
//величина піраміди
float MovementLimit; //
переміщення
bool BallsInMove=false;
int Player=0;
float CompAngle;
int CompMove;
float CalculateAngle;
int tick;
bool MustBeHitted;
int Balls;
int ballsIn;
TForm1 *Form1;
TCue *Cue=new TCue;
TBall *Ball=new TBall;
TBilliardTable
*BilliardTable= new TBilliardTable;
__fastcall
TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{}
void __fastcall
TForm1::StopAll(){
{ int i;
TBall *b = new TBall;
{ for( i =0;
i<=BilliardTable->Ball->Count-1; i ++)
{ //b=BilliardTable->Ball->Items[i];
b->dx = 0;
b->dy = 0; }
Cue->Visible = True; } }}
void ComputerMove(){
Cue->Visible = True;
CompAngle = CalculateAngle;
if(CompAngle >
Cue->Angle)
CompMove = 1;
else CompMove = -1;}
void __fastcall
TForm1::FormKeyDown(TObject *Sender, WORD &Key,
TShiftState Shift)
{{ int cur;
TBall *b= new TBall;
TBilliardTable
*BilliardTable= new TBilliardTable;
{ if(BallsInMove) exit;
if(! Cue->Visible ) exit;
if(Player == 1 ) exit;//
нельзя управлять во время хода компьютера!
// cur = Cue->ToBall->ID;
if(Key == VK_DOWN)
Cue->Angle = Cue->Angle
- Step;
if(Key == VK_UP)
Cue->Angle = Cue->Angle
+ Step;
if(Key == VK_RIGHT )
{ cur = (cur + 1) % Ball->count;
//b = Ball->Items[cur];
}while( b->exists);
if(Key == VK_LEFT )
do{ cur = (cur - 1 +
Ball->count) % Ball->count;
// b = Ball->Items[cur];
}while( b->exists);
if(Key == 32 ) if
(Cue->Visible) Cue->Hit();
if(Key == 13 )
ComputerMove();
if (Key = 13) StopAll();
// Cue->ToBall = Ball->Items[cur];
} }}
void __fastcall
TForm1::Timer1Timer(TObject *Sender)
{{ int cur, i, num;
TBall *b, *b2 = new TBall;
boolean allstopped;
TLose *lz = new TLose;
float d;
char* Mess;
{ tick++;
/* while( Cue.Angle >
6.28 do
Cue.Angle := Cue.angle -
6.28;
while( Cue.Angle < 0 do
Cue.Angle := Cue.angle +
6.28;
*/ if (CompMove != 0
&& Player == 1)
{ Cue->Angle = Cue->Angle
+ Step*CompMove;
if(abs(CompAngle-Cue->Angle)
<= Step)
{ CompMove = 0;
Cue->Angle = CompAngle;
MustBeHitted = True; } }
if(Player == 0 )
MustBeHitted = False;
if(MustBeHitted )
if(Cue->energy >
MaxEnergy/2 )
{ Cue->Hit();
MustBeHitted = False; } for(int
i =0; i <=Ball->count-1; i++)
{ // b = Ball->Items[i];
if(!b->exists ) continue;
if(b->InLose(num) )
{ b->Stop();
b->exists = False;
Balls--;
ballsIn++;
//lz = BilliardTable->Lose->Items[num];
lz->ballsInside++;
//PlayerN[Player]->balls++;
if(Balls == 1 )
{ GameStatus->gsGameOver();
Timer1->Enabled = False; }
cur = Cue->ToBall->ID;
do{
cur = (cur + 1) % Ball->Count;
b = Ball->Items[cur];
}while( b->exist);
Cue->ToBall = b; }
b->dx = b->dx*mu;
b->dy = b->dy*mu;
if((b->x+b->dx >
BilliardTable->Right-b->R)
|| (b->x+b->dx <
BilliardTable->Left+b->R)
b->dx = -b->dx * mu;
b->x = b->x +
b->dx;
if((b->y+b->dy >
BilliardTable->Bottom-b->R)
|| (b->y+b->dy <
BilliardTable->Top+b->R)
b->dy = -b->dy * mu;
b->y = b->y +
b->dy;
b2 = b->collisedWith;
b->outFrom(b2);
d = sqrt(sqr(b->dx)+sqr(b->dy));
if(d < MovementLimit )
{ b->Stop; }
Cue->energy = Trunc(MaxEnergy/2*cos(tick/5)+MaxEnergy/2)+1;
}
allstopped = True;
for( i = 0; i
<=Ball->Count - 1; i ++)
{ b = Ball->Items[i];
allstopped = (b->dx == 0)
&& (b->dy == 0) && allstopped; }
if(allstopped && MovedLater
)
{ Cue->visible = True;
if(ballsIn == 0
then Player = 1 - Player;
if(Player == 1 )
ComputerMove;
ballsIn = 0; }
MovedLater = !allstopped;
BallsInMove = !allStopped;
Draw; } }}
void __fastcall
TForm1::FormKeyPress(TObject *Sender, char &Key)
{ if(BallsInMove ) exit;
if(Key in ["i",
"I"] )
{ ShowID = ! ShowID; }
if(Player == 1 ) exit;//
нельзя управлять во время хода компьютера!
if(Key in ["h",
"H"] then ComputerMove;// help me!}
void __fastcall
TForm1::FormDestroy(TObject *Sender)
{ INI = TIniFile->Create(ExtractFilePath(ParamStr(0))+"\settings.ini");
INI->Writeint ("Phisics",
"ballSize", ballSize);
INI->Writeint ("Phisics",
"PocketSize", loseSize);
INI->Writeint ("Phisics",
"MaxEnergy", MaxEnergy);
INI->Writeint ("Phisics",
"CueLength", CueLength);
INI->Writeint ("Phisics",
"PyramidHeight", PyramidHeight);
INI->WriteFloat("Phisics",
"Friction", mu);
INI->WriteFloat("Phisics",
"AngleStep", Step);
INI->WriteFloat("Phisics",
"MovementLimit", MovementLimit);
INI->Writeint ("Phisics",
"TimeInterval", Timer1->Interval);}
void __fastcall
TForm1::Button1Click(TObject *Sender)
{ int i, j;
pBall b;
pLose luza;
boolean unique, inrect;
randomize;
Player = 0;
CompMove = 0;
Timer1->Enabled = True;
with Image1->Canvas do
{ Brush->Color = clWhite;
Pen->Color = clBlack;
Rectangle(0, 0, Width,
Height); }
BilliardTable->Ball->Clear;
BilliardTable->Lose->Clear;
balls = -1;
new(b);
inc(balls);
b->x = BilliardTable->Width
/ 4 + BilliardTable->Left;
b->y = BilliardTable->Height
/ 2 + BilliardTable->Top;
b->R = ballSize;//Random(20)+10;
b->col = clLtGray;//Random(clWhite);
b->dx = Random*2-1;
b->dy = Random*2-1;
b->ID = 0;
b->exist = True;
BilliardTable->Ball->Add(b);
PlayerN[0]->balls = 0;
PlayerN[1]->balls = 0;
GameStatus = gsGame;
loses = -1;
for( j = 0; j <=1; j ++)
for( i = -1; i <=1; i ++)
{ new(luza);
inc(loses);
luza->x = BilliardTable->Left
+ (BilliardTable->Width / 2)*(i+1);
luza->y = BilliardTable->Top
+ (BilliardTable->Height / 2)*(j*2);
luza->R = loseSize;
if(abs(i)==1 )
{ luza->x = trunc(luza->x
- i*luza->R*sqrt(2)/2);
luza->y = trunc(luza->y
-(j*2-1)*luza->R*sqrt(2)/2); }
luza->ballsInside = 0;
BilliardTable->Lose->Add(luza);
}
for( j = 1; j
<=PyramidHeight; j ++)
for( i = 1; i <=j; i ++)
{ new(b);
inc(balls);
b->R = ballSize; //Random(20)+10;
b->col = clLtGray; //Random(clWhite);//clLtGray;
b->dx = Random*2-1;
b->dy = Random*2-1;
b->ID = balls;
b->exist = True;
if(j % 2 != 0
then b->y = -((j-1) / 2)*2*b->R+(i-1)*2*b->R
+ H / 2
else b->y = -((j-1) / 2)*2*b->R+(i-1)*2*b->R
- b->R + H / 2;
b->y = b->y + dh;
b->x = (j-1)*2*b->R +
3 * BilliardTable->Width / 4 + dw + LoseSize;
BilliardTable->Ball->Add(b);
}
inc(Balls);
BilliardTable->Cue->ToBall
= BilliardTable->Ball->Items[0];
BilliardTable->Cue->angle
= 180*Pi/180;
BilliardTable->Cue->visible
= False;
StopAll;
void __fastcall
TForm1::FormCreate(TObject *Sender)
{ INI = TIniFile->Create(ExtractFilePath(ParamStr(0))+"\settings.ini");
ballSize = INI->Readint ("Phisics",
"ballSize", 10);
loseSize = INI->Readint ("Phisics",
"PocketSize", ballSize + 5);
MaxEnergy = INI->Readint ("Phisics",
"MaxEnergy", 20);
CueLength = INI->Readint ("Phisics",
"CueLength", 200);
PyramidHeight = INI->Readint
("Phisics", "PyramidHeight", 5);
mu = INI->ReadFloat("Phisics",
"Friction", 0.97);
Step = INI->ReadFloat("Phisics",
"AngleStep", 0.03);
MovementLimit = INI->ReadFloat("Phisics",
"MovementLimit", 0.01);
Timer1->Interval = INI->Readint
("Phisics", "TimeInterval", 20);
Width = Screen->Width-4;
Height = Screen->Height-4;
Panel1->Width = ClientWidth;
Panel1->Height = ClientHeight;
Image1->Width = ClientWidth;
Image1->Height = ClientHeight;
dw = (Image1->Width - W) /
2;
dh = (Image1->Height - H)
/ 2;
BilliardTable = TBilliardTable->Create;
BilliardTable->Ball = TList->Create;
BilliardTable->Lose = TList->Create;
BilliardTable->Cue = TCue->Create;
Left = dw + loseSize;
Top = dh + loseSize;
Width = W - LoseSize*2;
Height = H - LoseSize*2;
Right = Left + Width;
Bottom = Top + Height;
Player = 0;
Button1Click(Sender);}
2.2 Опис гри
Грають двоє: людина і
комп'ютер. Першим робить хід чоловік. Хід передається іншому гравцеві, якщо даний
гравець не забив в лузи жодної кулі (див. Додаток А).
Сила удару залежить від
відстані в даний момент кия від битка (див. Додаток Б).
Управління:
Курсори:
вгору-вниз - обертання кия
вліво-управо - перемикання з однієї кулі на іншій
"пропуск" - удар києм
"H",
"h" - підказка для людини (як на його місці
зробив би хід чоловік)
"I",
"i" - включення/виключення нумерації куль
"S",
"s" - включення/виключення лінії прицілювання
Опис файлу конфігурації
settings.ini:
ballsize=10 - розмір куль
Pocketsize=20 - розмір лузи
Maxenergy=20 - максимальна сила удару
Cuelength=200 - довжина кия
Friction=0,97 - коефіцієнт тертя (строго менше 1)
Pyramidheight=5 - кількість рівнів в піраміді з кулями
Anglestep=0,03 - крок повороту кия навколо кулі
Movementlimit=0,1 - межа вектора швидкості, після якого рух кулі вважається припиненим.
Timeinterval=20 - час між кадрами перемальовування (у мілісекундах)
Висновок
Використання методів Canvas для відображення
графіки в проектах C++Builder допомогло реалізувати поставлену задачу. Але цей
метод від малювання графіки на формі об’єктів є досить не практичний і тому
важливо кожного разу перемалювати всю сцену з її об’єктами, а коли ми маємо
анімацію то перемалювання сцени має ще й відбуватись непомітно для ока
користувача, хоча цього часом буває досить важко добитись, особливо коли багато
анімацій відбувається одночасно для декількох обєктів, що збільшує час виводу
певного зображення на екран.
В даній роботі я зміг
добитись пере малювання куль, кия та всього столу буз затримки картинки, що
створює ілюзію анімації для людського ока. Сама логіка гри дуже проста, коли
кілі торкаються одна одної то кожній передається імпульс і прискорення з початковою
швидкістю, котра зменшується з часом та відбиттям від інших об’єктів, тобто
зіткненням.
Програма широко
використовує фізичні закони, для моделювання гри в середовищі C++Builder.
Використана література
1.
С++ для начинающих Липпман
2003г 332 стр.
2.
Введение в язык С++ Бьярн
Страустрап, 1995 г. ; Книга по Си; уроки Visual C++ 2004г, 560 стр.
3.
http://forums.delphi.com/ab_cplus/start
4.
Программирование на языке
СИ Ю.Ю.Громов, С.И.Татаренко 1998г 545 стр.;
5.
Applied C++: Practical
Techniques for Building Better Software Авторы: Philip Romanik, Amy Muntz 2003г. 470 стр.
6.
C++ Unleashed Автор: Jesse
Liberty 2005г. 396 p.
|