Costas-Tyros_rus_MS
.pdf
Глава 14: Lсистемы
Аристид Линденмайер (Aristid Lindenmayer) – датский ботаник, который изобрёл умный способ описания форм растений. Как вы знаете, ботаники тратят большое количество времени, описывая
форму растений. Их описания обычно делаются на упрощённой версии латыни, изобретённой Кар лом Линнеем (Carl Linaeus).
Линденмайеровский метод описания формы растений основывается на рисующей черепахе, ко торую вы изучили в разделе 13.1. В интернете вы найдёте огромное количество материала по этой теме. Таким образом, я ограничу себя примером, который вы можете использовать как стартовую точку для ваших собственных проектов.
•Создайте GUI проект: Lsys.
•Добавьте пакет к корню дерева проекта: aristid.
•Добавьте форму к пакету aristid: canvas.
•Постройте приложение
•Добавьте класс к aristid: draw. Снимите галочку Create Objects. Постройте проект, что бы вставить класс в дерево проекта.
•Включите пункт File/New панели задач. Добавьте
clauses
onFileNew(S, _MenuTag) :
X= canvas::new(S), X:show().
кTaskWindow.win/CodeExpert/Menu/TaskMenu/id_file/id_file_new.
14.1.Класс draw
Затем, отредактируйте класс draw как показано ниже. Постройте приложение.
%Файл draw.cl class draw
open core, vpiDomains predicates
classInfo : core::classInfo. tree:(windowHandle).
end class draw
%Файл draw.pro
implement draw
open core, vpiDomains, vpi, math clauses
classInfo("plotter/draw", "1.0"). domains
command = t(commandList); f(integer); r(integer); m. commandList= command*.
class facts
pos:(real Delta, real Turn) single. grammar:(commandList) single.
class predicates
move:(windowHandle, pnt, real, commandList) procedure (i, i, i, i).
111
derive:(commandList, commandList) procedure (i, o).
mv:(windowHandle, pnt, real, command, pnt, real) procedure (i, i, i, i, o, o).
iter:(integer, commandList, commandList) procedure (i, i, o).
clauses
pos(4.0, 0.1745329). grammar([f(1)]).
iter(0, S, S) : !. iter(I, InTree, OutTree) :
derive(InTree, S), iter(I 1, S, OutTree).
derive([], []) : !. derive([f(0)|Rest], [f(0),f(0)|S]) : !,
derive(Rest, S). derive([f(_)|Rest],
[f(0), t([r(1),f(1)]), f(0), t([r( 1),f(1)]), r(1), f(1)|S]) : !,
derive(Rest, S). derive([t(Branches)|Rest], [t(B)|S]) : !,
derive(Branches, B), derive(Rest, S).
derive([X|Rest], [X|S]) : derive(Rest, S).
mv(Win, P1, Facing, f(_), P2, Facing) : !, pos(Displacement, _Turn),
P1= pnt(X1, Y1),
X2= round(X1+ Displacement*cos(Facing)), Y2= round(Y1+Displacement*sin(Facing)), P2= pnt(X2, Y2),
drawline(Win, P1, P2).
mv(_Win, P1, Facing, m, P2, Facing) : !, pos(Displacement, _Turn),
P1= pnt(X1, Y1),
X2= round(X1+ Displacement*cos(Facing)), Y2= round(Y1+Displacement*sin(Facing)), P2= pnt(X2, Y2).
mv(_Win, P1, Facing, r(Direction), P1, F) : !, pos(_Displacement, Turn),
F= Facing+ Direction*Turn.
mv(Win, P1, Facing, t(B), P1, Facing) : move(Win, P1, Facing, B).
move(_Win, _P1, _Facing, []) : !. move(Win, P1, Facing, [X|Rest]) : mv(Win, P1, Facing, X, P, F),
move(Win, P, F, Rest).
tree(Win) :
112
grammar(Commands), iter(5, Commands, C), Facing= pi/2, Point= pnt(100, 250),
move(Win, Point, Facing, C). end implement draw
Наконец, добавьте фрагмент
clauses
onPaint(S, _Rectangle, _GDIObject) : draw::tree(S:getVPIWindow()).
к PaintResponder. Постройте и запустите приложение.
Откройте форму для рисования, выбрав пункт File/New из меню приложения. Она покажет дере во, такое, как на рисунке.
14.2.Примеры
Пример этой главы находятся в папке Lsys.
113
Глава 15: Игры
В этой главе вы научитесь реализовывать игры в Visual Prolog. Конечно же, хорошая игра это как хорошая поэма. Недостаточно выучить русский язык и правила стихосложения для того, чтобы писать как Пушкин. Поэт, кроме знания языка, на котором он будет писать, нуждается в воображении, креа тивности, и т.д. То же самое относится к хорошему разработчику игр. Вам нужно много творчества для того, чтобы сделать что то наподобие Tetris или Space Invaders. Я использовал эти примеры для того, чтобы показать, что хорошие игры не зависят от прихотливой графики или звуковых эффектов. Важными особенностями являются тревожное ожидание, длительное напряжение и ощущение на грады за хорошую работу.
Однажды, молодой человек спросил Лопе де Вега (Lope de Vega), как писать стихи. Ответ был та кой: Это просто: Заглавная буква в начале каждой строфы и рифма в конце. «Что же мне вста вить в середину?», спросил парень. В середину, hay que poner talento1, сказал Лопе де Вега.
В этом уроке рассматривается несколько техник, подобных заглавным буквам и рифме. Чтобы получить хорошую игру, вам потребуется добавить к рецепту талант.
Игра, которую вы реализуете, простая. Есть червяк, быстро ползущий по экрану. Игрок должен изменять направление его движения для того, чтобы он не врезался в стены и препятствия. Вам не обходимо реализовать следующие элементы:
1.Окно, по которому червяк будет ползать. Окно должно иметь четыре кнопки для управ ления движением червяка.
2.Класс состояния, который описывает направление движения и положение червя. Он экс портирует предикаты mov/0, для изменения положения червя, и turn/2, который может быть использован для его поворотов.
3.Класс, который рисует изображение положения червя. Он экспортирует предикат snap shot(Win). Обратите внимание на следующую деталь: предикат snapshot/1 не может нарисовать червя прямо на окне. Это бы заставило экран мерцать. Он должен построить изображение, и только затем нарисовать его на экране.
4.Таймер, который вызывает mov/0 и snapshot/1 через фиксированные интервалы, для создания иллюзии движения.
Мы знаем, что делать; давайте это делать. До этого момента я избегал создания объектов. Дей
ствительно, каждый раз, когда вы создавали класс, вам рекомендовалось снимать галочку Creates Objects. Однако, если мы жестко будем следовать этой установке, наша игра будет иметь очень не приятное свойство. Если вы проиграете, выйдете из текущей игры и попробуете начать новую, вы найдёте, что новая игра не в своём изначальном состоянии. На самом деле она будет в состоянии, где вы проиграли старую. Проблема в том, что переменные, используемые для описания состояния, имеют память, которая переходит от одной формы к другой. Объекты были изобретены для предот вращения такого рода неприятного поведения. Поэтому, давайте используем эту очень хорошую возможность изучить, как создавать объекты и работать с их состояниями.
•Создайте проект: game.
Project Name: game
UI Strategy: Object Oriented GUI (pfc/vpi)
TargetType: Exe
Base Directory: C\vip70
Sub Directory: game
•Создайте пакет: playground.
1 исп. «нужно поместить талант»
114
Рисунок 15.1 Форма лужайка
•Создайте форму: lawn. Вы добавите четыре кнопки к форме lawn; обращайтесь к рис.
15.1 за оформлением. Хорошей идеей будет сменить названия кнопок в диалоговом окне
Properties с pushButton_ctl на up_ctl, down_ctl, left_ctl, и right_ctl. Постройте при ложение для того, чтобы включить в него эту форму. Затем включите File/New и добавьте
clauses
onFileNew(S, _MenuTag) : X= lawn::new(S), X:show().
кProjWin/Code Expert/Menu/TaskMenu/id_file/id_file_new.
•Создайте класс objstate внутри playground. Не отключайте Create Objects для класса objstate. Внесите в него код, показанный на рис. 15.3; NB: Класс objstate должен иметь интерфейс в файле objstate.i. Постройте приложение.
•Создайте класс draw внутри playground. Отключите Create Objects. Внесите код с рис. 15.2. Постройте приложение.
•Создайте класс click для хранения часов. Снимите Creates Objects.
%File: click.cl class click
open core, vpiDomains predicates
bip:(window). kill:().
end class click
% File:click.pro implement click open core
class facts
tm:vpiDomains::timerId := null. clauses
bip(W) : tm := W:timerSet(500). kill() : vpi::timerKill(tm).
end implement click
Постройте приложение для включения click в проект.
115
•Добавьте
clauses
onDestroy(_Source) : click::kill().
кDestroyListener в диалоговом окне Properties формы lawn.frm.
•Добавьте
class facts
stt:objstate := objstate::new(). clauses
onShow(Parent, _CreationData) : stt := objstate::new(), stt:init(), click::bip(Parent).
кShowListener в диалоговом окне Properties формы lawn.frm.
•Добавьте код для ClickResponder, соответствующему кнопкам:
clauses
onDownClick(_S) = button::defaultAction() : stt:turn(0, 10).
clauses
onLeftClick(_S) = button::defaultAction() : stt:turn( 10, 0).
clauses
onRightClick(_S) = button::defaultAction() : stt:turn(10, 0).
clauses
onUpClick(_S) = button::defaultAction() : stt:turn(0, 10).
•Добавьте
clauses
onTimer(_Source, _TimerID) : stt:mov(), R= rct(10, 10, 210, 210), invalidate(R).
кTimeListener в диалоговом окне Properties формы lawn.frm.
•Добавьте
clauses
onPaint(_Source, _Rectangle, GDIObject) : draw::snapshot(GDIObject, stt).
к PaintResponder в диалоговом окне Properties формы lawn.frm.
Постройте и запустите программу. После выбора пункта File/New, выскочит окно с червяком. Вы можете управлять червяком, используя четыре кнопки.
Улучшение игры остаётся за вами. Вы можете добавить препятствия, например стены. Вы также можете положить маленькие яблоки для питания червя. Наконец, вы можете написать 3D игру. 3D игра на самом деле очень проста. Сегменты имеют три координаты, и вам нужно найти их проекции на плоскость, прежде чем рисовать. Если вы поищете информацию в интернете, вы определённо
116
найдёте формулы для вычисления перспективной проекции точки. Алекс Соарес (Alex Soares) написал оригинальную игру, на которой я основал это руководство. Его игра была в 3D, в Visual Prolog 5.2, ис пользуя DirectX – библиотеку, содержащую много ресурсов для 3D игр.
%Файл draw.cl class draw
open core, vpiDomains predicates
classInfo : core::classInfo. snapshot:(windowGDI Win, objstate).
end class draw
%Файл draw.pro
implement draw
open core, vpiDomains, vpi class predicates
draw:(windowHandle, objstate) procedure. clauses
classInfo("playground/draw", "1.0"). draw(W, S) : S:segm(Rectangle),
vpi::drawEllipse(W, Rectangle), fail. draw(_W, _S).
snapshot(Win, S) : S:mov(), !, Rectangle= rct(0, 0, 200, 200), W= pictOpen(Rectangle), draw(W, S),
Pict= pictClose(W),
Win:pictDraw(Pict, pnt(10, 10), rop_SrcCopy). end implement draw
Рисунок 15.2 draw.cl и draw.pro
Давайте продолжим с нашим изучением предикатов рисования. Если вы рисуете фигуры на каж дый тик часов, вы создаёте анимацию. Однако, если вы производите рисование прямо на видимом окне, анимация будет мерцать, что не очень хорошо. Решение этой проблемы – рисование на спря танном окне, и передача его в видимое окно только после того, как оно будет готово к отображению. Предикат
W= pictOpen(Rectangle)
открывает скрытое окно для того, чтобы вы рисовали, не беспокоясь о мерцающей анимации. Предикат
Pict= pictClose(W)
создаёт изображение из содержимого скрытого окна в то же время он закрывает окно. Наконец, предикат
pictDraw(Win, Pict, pnt(10, 10), rop_SrcCopy)
передаёт изображение в видимое окно. Вот пример кода, содержащего эти три предиката:
117
snapshot(Win, S) : S:mov(), !,
Rectangle= rct(0, 0, 200, 200), W= pictOpen(Rectangle), draw(W, S),
Pict= pictClose(W),
Win:pictDraw(Pict, pnt(10, 10), rop_SrcCopy).
Аргумент rop_SrcCopy определяет режим растровых операций. Он говорит, что система должна скопировать рисунок из исходного окна в назначенное. Есть и другие режимы передачи изображе ний из одного окна в другое:
•rop_SrcAnd, производит логическое И между битами рисунка и фоном. Вы можете ис пользовать это для создания спрайтов.
•rop_PatInvert, инвертирует цвета.
•rop_SrcInvert инвертирует цвета источника.
15.1.Объектныефакты
Обратите внимание на одну специфическую деталь: когда мы имеем дело с классами, которые не создают объектов, состояние объекта устанавливается фактами класса (class facts); вот как факт класса объявляется:
class facts
city:(string Name, pnt Position). conn:(pnt, pnt).
clauses
city("Salt Lake", pnt(30, 40)). city("Logan", pnt(100, 120)). city("Provo", pnt(100, 160)).
conn(pnt(30, 40) , pnt(100, 120)). conn(pnt(100, 120), pnt(100, 160)).
Факт класса является общим для всех объектов класса. Это означает, что если объектный преди кат (предикат, объявленный в интерфейсе) изменяет факт, он изменяется для всех объектов класса. Поэтому, если сохранять состояние червя в факте класса, игра бы имела следующую неприятную особенность:
Неприятная особенность игры – Когда игрок потеряет червя, выйдет из текущей игры, и начнёт играть в следующую, он находит, что новый червяк не в изначальном положении. На самом деле, он расположен в той позиции, в которой был потерян старый.
Проблема в том, что факты класса, используемые для описания состояния червя, имеют память класса (class memory), которая остаётся от одной формы к другой, от одного объекта червя к другому.
Решение простое: объявляйте факты без добавления ключевого слова class к заголовку объявления; тогда у каждого объекта будет собственный набор фактов. Вот как объявить объектные факты:
facts
w:(integer, pnt) nondeterm. % червяк d:(integer, integer) single. % направление
clauses
init() : assert(w(1, pnt(110, 100))),
assert(w(2, pnt(120, 100))), assert(w(3, pnt(130, 100))), assert(w(4, pnt(140, 100))), assert(w(5, pnt(140, 100))).
118
d(10, 0).
Эта схема сохранения состояния объекта реализована в программе на рис. 15.3.
% Файл objstate.cl
class objstate : objstate end class objstate
%Файл objstate.i interface objstate
open core, vpiDomains predicates
init:().
turn:(integer, integer). mov:().
segm:(rct) nondeterm (o). end interface objstate
%Файл objstate.pro implement objstate
open core, vpiDomains facts
w:(integer, pnt) nondeterm. % worm d:(integer, integer) single. % direction
clauses
init() : assert(w(1, pnt(110, 100))), assert(w(2, pnt(120, 100))), assert(w(3, pnt(130, 100))), assert(w(4, pnt(140, 100))), assert(w(5, pnt(140, 100))).
d(10, 0).
mov() : retract(w(1, P)), P= pnt(X1, Y1),
d(DX, DY), P1= pnt(X1+DX, Y1+DY), assert(w(1, P1)), retract(w(2, P2)), assert(w(2, P)),
retract(w(3, P3)), assert(w(3, P2)), retract(w(4, P4)), assert(w(4, P3)), retract(w(5, _)), assert(w(5, P4)), !.
mov().
segm(rct(X, Y, X+10, Y+10)) : w(_, pnt(X, Y)). turn(DX, DY) : assert(d(DX, DY)).
end implement objstate
Рисунок 15.3 Класс objstate
119
Глава 16: Анимация
Пожалуйста, взгляните на главу 8, если вы не помните, как пользоваться событием painting. Вам также понадобится использовать маски для создания рисунков с прозрачным фоном; эта тема тоже была рассмотрена в главе 8. Вы также можете вспомнить использование GDIObject, которое сделано в разделе 3.2, где вы также найдёте материал об обновлении (invalidating) прямоугольника для ри сования.
•Создайте проект
Project Name: rattlesnake
UI Strategy: Object oriented GUI (pfc/gui)
В этом проекте вы анимируете знаменитую гремучую змею, которая присутствует на флаге Джо на Пол Джонса (John Paul Jones) «Не наступите на меня!» (Don’t thread on me).
•Создайте новый пакет, и пусть его имя будет snake.
•Создайте новую форму canvas. Вложите её в пакет snake.
•Создайте класс click для хранения событий часов. Отключите Creates Objects. Используй те код, данный на рис. 16.1 для определения и реализации click.
•Постройте приложение. Включите File/New. Затем, добавьте
onFileNew(S, _MenuTag) :
F= canvas::new(S), F:show(), click::bip(F).
к TaskWindow.win/CodeExpert/Menu/TaskMenu/id_file/id_file_new.
%Файл click.cl class click
open core predicates
bip:(window). kill:(window).
end class click
%Файл click.pro implement click
open core class facts
tm:vpiDomains::timerId := null. clauses
bip(W) : tm := W:timerSet(1000). kill(W) : W:timerKill(tm).
end implement click
Рисунок 16.1 Класс click разбирается с таймером
120
