Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Объектно-ориентированное программирование.PDF
Скачиваний:
208
Добавлен:
01.05.2014
Размер:
3.64 Mб
Скачать

converted to PDF by BoJIoc

или N=3). Что будет в этом случае с нашими программами? Как можно организовать более осмысленный вывод ответа?

4.Используя графические возможности вашей системы, измените одну из программ так, чтобы она динамически изображала на шахматной доске позиции каждого ферзя по мере своей работы. Какие части кода должны знать об устройстве вывода?

Глава 6

Учебный пример: игра «Бильярд»

Во втором примере мы построим простую имитацию бильярдного стола. Программа написана на языке Object Pascal для Macintosh 1 . Как и в случае с восемью ферзями, разработка делает упор на создание автономных агентов, взаимодействующих между собой для достижения желаемого результата.

6.1. Элементы бильярда

Для пользователя бильярдный стол представляет собой окно, содержащее прямоугольник с лузами по углам, 15 черных шаров и 1 белый шар. Нажатием кнопки мыши пользователь имитирует удар кием по шару, сообщая ему некоторую энергию. Шар движется в сторону, противоположную указателю мыши. Получив энергию, шар начинает катиться, отскакивая от стенок, ударяя другие шары, и, наконец, попадает в лузу. Когда один шар сталкивается с другим, часть энергии первого передается второму и в результате направление движения обоих шаров меняется.

1 Программа «Бильярд» была разработана на компьютере PowerPC Macintosh с использованием компилятора CodeWarrior Pascal версии 1.1. В версии для PowerPC движение столь быстро, что я решил вставить в процедуру Ball.update замедляющий цикл, несколько раз вызывающий метод draw. Игра, реализованная программой из этой главы, не соответствует никакой настоящей

converted to PDF by BoJIoc

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

6.2. Графические объекты

Основу имитации составляют три списка графических объектов, представляющих стенки, лузы и шары. Каждый графический объект включает в себя поле ссылки и поле, описывающее местоположение объекта на экране 1 .

Мы ввели упрощающее предположение, что все графические объекты занимают прямоугольную область. Это, конечно, совершенно неверно для круглых объектов наподобие шара. Более реалистичной альтернативой было бы написать процедуру, определяющую, пересеклись ли два шара, на основе их действительной геометрии. Но сложность процедуры только отвлекла бы нас от того главного, чего мы хотим добиться, приводя этот пример, а именно понять способ наделения объектов ответственностью за их поведение. Каждый графический объект знает не только как изображать себя, но и как взаимодействовать с другими объектами нашей модели мира.

6.2.1. Графический объект Wall (стенка)

Первым из наших трех графических объектов является стенка Wall. Она определяется следующим описанием класса:

Wall = object

(* поля данных *) link : Wall; region : Rect;

(* угол отскока шаров *) convertFactor : real;

(* инициализирующая функция *) procedure initialize

(left, top, right, bottom : integer; cf : real); (* изображение стенки *)

procedure draw;

(* сообщение стенке, что о нее ударился шар *) procedure hitBy (aBall : Ball);

end;

1 Неясно, куда поместить изучение этого примера. С одной стороны, читателю важно как можно быстрее увидеть применение объектных принципов; поэтому желательно, чтобы этот пример встретился в книге пораньше. С другой стороны, эта программа только выиграла бы от более изощренной техники, которая обсуждается ниже. В частности, графические объекты было бы лучше представлять в виде иерархии наследования, как описано в главе 7. Кроме того,

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

Поле link (ссылка) служит для поддержания списка объектов Wall. Инициализирующий метод просто задает местоположение (region) стенки и параметр отскока (convert factor):

procedure Wall.initialize

(left, top, right, bottom : integer; cf : real); begin

(* инициализация convertFactor *) convertFactor := cf;

(* установить область для стены *)

converted to PDF by BoJIoc

SetRect (region, left, top, right, bottom); end;

Стенка может быть нарисована просто как сплошной прямоугольник. Это выполняется стандартной процедурой для Macintosh:

procedure Wall.draw; begin

PaintRect (region); end;

Самое интересное происходит со стенкой, когда о нее ударяется шар. Направление его движения изменяется, основываясь на значении параметра convertFactor для стенки. (Переменная convertFactor равна или нулю, или pi, в зависимости от того, горизонтальная стенка или вертикальная.) В результате столкновения шар будет двигаться в новом направлении.

procedure Wall.hitBy (aBall : Ball); begin

(* оттолкнем шар от стенки *) aBall.setDirection(convertFactor — aBall.direction);

end;

6.2.2. Графический объект Hole (луза)

Hole (луза) определяется следующим описанием класса:

Hole = object

(* поля данных *) link : Hole; region : Rect;

(* инициализирующая функция *) procedure initialize (x, y : integer); (* изображение лунки *)

procedure draw;

(* сообщение лузе, что в нее попал шар *) procedure hitBy (aBall : Ball);

end;

Как и в случае стенок, инициализация и изображение лузы в основном состоят из вызова соответствующих стандартных подпрограмм:

procedure Hole.initialize (x, y : integer); begin

(* определить область с центром в x, y *) SetRect(region, x-5, y-5, x+5, y+5);

end;

procedure Hole.draw; begin

PaintOval (region); end;

Больший интерес представляет происходящее при «ударе» шара о лузу. Есть два случая. Если шар оказался белым (он идентифицируется глобальной переменной CueBall), то он возвращается назад в игру на определенную позицию. В остальных случаях шар лишается энергии и убирается со стола в специальную область.

procedure Hole.hitBy (aBall : Ball); begin

(* остановить шар; убрать его со стола *) aBall.energy := 0.0;

aBall.erase;

(* передвинуть шар *)

converted to PDF by BoJIoc

if aBall = CueBall then aBall.setCenter(50.100); else begin

saveRack := saveRack + 1;

aBall.setCenter (10 + saveRack * 15, 250); end;

(* перерисовать шар *) aBall.draw;

end;

6.2.3. Графический объект Ball (шар)

Последним графическим объектом является шар, определяемый следующим описанием класса:

Ball = object

(* поля данных для шара *) link : Ball;

region : Rect;

direction : real; (* направление в радианах *) energy : real;

(* инициализирующая функция *) procedure initialize (x, y : integer); (* методы *)

procedure draw; procedure erase; procedure update;

procedure hitBy (aBall : Ball);

procedure setDirection (newDirection : real); (* возвращают x, y — координаты центра шара *) function x : integer;

function y : integer; end;

В дополнение к полям ссылки (link) и местоположения (region), общими с остальными объектами, шар имеет два новых поля данных: direction (направление), вычисленное в радианах, и energy (энергия), представляющее собой вещественное значение. Как и в случае лузы, шар инициализируется аргументами, описывающими координаты его центра. Первоначально шар не имеет энергии и его направление нулевое.

procedure Ball.initialize (x, y : integer); begin

SetRect (region, x-5, y-5, x+5, y+5); setDirection (0.0);

energy := 0.0; end;

Шар изображается либо окружностью, либо сплошным кругом, в зависимости от того, является ли он белым или нет.

procedure Ball.draw; begin

if self = CueBall then (* рисуем окружность *) FrameOval (region); else

(* рисуем круг *) PaintOval (region);

end;

procedure Ball.erase;

converted to PDF by BoJIoc

begin

EraseRect (region); end;

Метод update используется для изменения позиции шара. Если он имеет заметную энергию, то слегка сдвигается, а затем проверяет, не задел ли он другой объект. Глобальная переменная ballMoved устанавливается в true, если какой-либо шар на столе сдвинулся. Если шар задел другой объект, шар сообщает об этом объекту. Сообщения бывают трех видов; они соответствуют ударам по лузе, стенке и другим шарам. Наследование, которое мы изучаем в главе 7, предоставляет методы объединения этих трех тестов в один цикл.

procedure Ball.update; var

hptr : Hole; wptr : Wall; bptr : Ball;

dx, dy : integer; theIntersection : Rect;

begin

if energy > 0.5 then begin

ballMoved := true; (* удалить шар *) erase;

(* уменьшить энергию *) energy := energy — 0.05; (* сдвинуть шар *)

dx := trunc(5.0 * cos(direction)); dy := trunc(5.0 * sin(direction)); offsetRect(region, dx, dy);

(* перерисовать шар *) draw;

(* проверить, не попали ли в лузу *) hptr := listOfHoles;

while (hptr <> nil) do

if SectRect (region, hptr.region, theIntersection) then

begin hptr.hitBy(self); hptr := nil;

end else

hptr := hptr.link;

(* проверить, не ударились ли в стенку *) wptr := listOfWalls;

while (wptr <> nil) do

if SectRect (region, wptr.region, theIntersection) then

begin wptr.hitBy(self); wptr := nil;

end else

wptr := wptr.link;

(* проверить, не ударили ли шар *) bptr := listOfBalls;

while (bptr <> nil) do

converted to PDF by BoJIoc

if SectRect (region, bptr.region, theIntersection) then

begin bptr.hitBy(self); bptr := nil;

end else

bptr := bptr.link;

end;

end;

Когда один шар ударяет другой, энергия первого делится пополам между ними. Также меняются направления движения обоих шаров.

procedure Ball.hitBy (aBall : Ball); var

da : real; begin

(* уменьшить энергию ударяющего шара наполовину *) aBall.energy := aBall.energy / 2;

(* и добавить ее к нашему шару *) energy := energy + aBall.energy;

(* установить наше новое направление *) setDirection(hitAngle(self.x aBall.x,

self.y — aBall.y);

(* и направление ударяющего шара *) da := aBall.direction — direction;

aBall.setDirection (aBall.direction + da); end;

function hitAngle (dx, dy : real) : real; const

PI = 3.14159; var

na : real; begin

if (abs(dx) < 0.05) then na := PI / 2;

else

na := arctan (abs(dy / dx)); if (dx < 0) then

na := PI — na; if (dy < 0) then na := -na; hitAngle := na;

end;

6.3. Основная программа

В предыдущем параграфе описывались статические характеристики программы. Динамика начинается при нажатии кнопки мыши. При этом вызывается следующая процедура:

procedure mouseButtonDown (x, y : integer); var

bptr : Ball; begin

(* присвоим белому шару некоторую энергию *) CueBall.energy := 20.0;

(* и направление *)

converted to PDF by BoJIoc

CueBall.setDirection(hitAngle (CueBall.x — x, CueBall.y — y));

(* изменения происходят, пока движется хотя бы один шар *) ballMoved := true;

while ballMoved do begin

ballMoved := false; bptr := listOfBalls; while bptr <> nil do begin

bptr.update;

bptr := bptr.link; end;

end;

end;

Оставшаяся часть программы относительно прямолинейна и не представлена здесь. Весь текст находится в Приложении Б. Основная часть кода связана с инициализацией новых объектов и организацией цикла ожидания события, то есть действия пользователя.

Главное понять то, как было децентрализовано управление и как сами объекты были наделены возможностями влиять на ход выполнения программы. Все, что происходит при нажатии кнопки мыши, — это наделение белого шара некоторой энергией. В дальнейшем модель руководствуется исключительно взаимодействием шаров.

6.4. Использование наследования

В главе 1 мы описали наследование неформально, а в главе 7 обсудим, как оно работает

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

вернуться к этому параграфу после ознакомления с общими положениями о наследовании

вследующей главе.

Первым шагом в использовании наследования в нашей имитации бильярда является описание общего класса «графический объект». Он породит трех потомков: шары, стенки и лузы. Родительский класс определяется следующим образом:

GraphicalObject = object (* поля данных *)

link : GraphicalObject; region : Rect;

(* инициализирующая функция *)

procedure setRegion (left, top, right, bottom : integer);

(* операции, выполняемые графическими объектами *) procedure draw;

procedure erase; procedure update;

function intersect (anObj : GraphicalObject) : boolean;

procedure hitBy (anObj : GraphicalObject); end;

Инициализирующая функция setRegion просто устанавливает область, занимаемую объектом. Методы draw и update ничего не делают, так как их фактическое поведение определено в дочерних классах. Программа erase очищает область, занимаемую объектом. intersect возвращает значение true, если объект-аргумент пересекается с рассматриваемым объектом. И наконец, метод hitBy также переопределяется в дочерних классах. Хотя двигаются только шары и, следовательно, аргументом этой функции всегда

converted to PDF by BoJIoc

будет шар, тот факт, что класс Ball еще не определен, означает, что мы должны объявить аргумент как имеющий более общий тип GraphicalObject:

procedure GraphicalObject.setRegion (left, top, right, bottom : integer);

begin

SetRect(region, left, top, right, bottom); end;

procedure GraphicalObject.draw; begin

(* переопределяется в дочернем классе *) end;

procedure GraphicalObject.erase; begin

EraseRect (region); end;

procedure GraphicalObject.update;

begin (* переопределяется в дочернем классе *) end;

procedure GraphicalObject.hitBy(anObject : GraphicalObject);

begin (* переопределяется в дочернем классе *) end;

function GraphicalObject.intersect (anObject : GraphicalObject) : boolean;

var

theIntersection : Rect; begin

intersect := SectRect

(region, anObject.region, theIntersection); end;

Теперь Ball, Wall и Hole объявляются как подклассы общего класса GraphicalObject, и внутри них ни к чему объявлять данные или функции, если только они не переопределяются:

Hole = object (GraphicalObject)

(* инициализация местоположения лузы *) procedure initialize (x, y : integer); (* изображение лузы *)

procedure draw; override;

(* сообщить лузе, что в нее попал шар *) procedure hitBy (anObject : GraphicalObject);

override;

end;

Процедура hitBy должна преобразовать тип аргумента в Ball. Благоразумно проверить тип до приведения:

procedure Wall.hitBy (anObj : GraphicalObject); var

aBall : Ball; begin

if Member (anObj, Ball) then begin

aBall := Ball(anObj); aBall.setDirection(convertFactor — aBall.direction); end;

end;