haskell-notes

Тип IO обычно отвечает за связь с внешним миром, это глаза, уши, руки и ноги программы. Через IO мы

получаем информацию из внешнего мира и отправляем её обратно. Но в нашем случае он проник в сердце

программы. За обновление объектов отвечает насыщенная IO библиотека Hipmunk.

Мы постараемся побороться с IO-кодом так. Сначала мы выделем те параметры, которые могут быть

обновлены чистыми функциями. Это все те параметры, для которых не нужен Hipmunk. Этот шаг разбивает

наш мир на два лагеря: “чистый” и “грязный”:

data World = World

{ worldPure

:: Pure

, worldDirty

:: Dirty }

Чистые данные хотят как-то узнать о том, что происходит в грязных данных. Также чистые данные могут

рассказать грязным, как им нужно измениться. Это приводит нас к определению двух языков запросов, на

которых чистый и грязный мир общаются между собой:

data Query = Remove Ball | HeroVelocity H.Velocity | MakeBall Freq

data Event = Touch Ball | UserClick H.Position

data Sense = Sense

{ senseHero

:: HeroBall

, senseBalls

:: [Ball]

}

Через Query чистые данные могут рассказать грязным о том, что необходимо удалить шар из игры, об-

новить скорость шара игрока или создать новый шар (Freq отвечает за параметры создания шара). Грязные

данные могут рассказать чистым на языке Event и Sense о том, что один из шаров коснулся до шара иг-

рока, или игрок кликнул мышкой в определённой точке. Также мы сообщаем все обновлённые положения

параметры шаров в типе Sense. Тип Event отвечает за события, которые происходят иногда, а тип Sense за

те параметры, которые мы наблюдаем непрерывно (это типы глазарук), Query – это язык действий (это тип

руконог). Нам понадобится ещё один маленький язык, на котором мы будем объясняться с OpenGL.

data Picture = Prim Color Primitive

| Join Picture Picture

data Primitive = Line Point Point | Circle Point Radius

data Point

= Point Double Double

type Radius = Double

data Color = Color Double Double Double

Эти три языка станут барьером, которым мы ограничим влияние IO. У нас будут функции:

percept

:: Dirty -> IO (Sense, [Event])

updatePure

:: Sense -> [Event] -> Pure -> (Pure, [Query])

react

:: [Query] -> Dirty -> IO Dirty

updateDirty :: Dirty -> IO Dirty

picture

:: Pure -> Picture

draw

:: Picture -> IO ()

Вся логика игры будет происходить в чистой функции updatePure, обновлять модель мира мы будем в

updateDirty. Давайте опять начнём проектироваание сверху-вниз. С этими функциями мы уже можем напи-

сать основную функцию цикла игры:

loop :: IORef World -> IO ()

loop worldRef = do

world <- get worldRef

300 | Глава 20: Императивное программирование

drawWorld world

(world, dt) <- updateWorld world

worldRef $= world

G. addTimerCallback (max 0 $ frameTime dt) $ loop worldRef

updateWorld :: World -> IO (World, Time)

updateWorld world = do

t0 <- get G. elapsedTime

(sense, events) <- percept dirty

let (pure’, queries) = updatePure sense events pure

dirty’ <- updateDirty =<< react queries dirty

t1 <- get G. elapsedTime

return (World pure’ dirty’, t1 t0)

where dirty = worldDirty world

pure

= worldPure

world

drawWorld :: World -> IO ()

drawWorld = draw . picture . worldPure

20.3 Определяемся с типами

Давайте подумаем, из чего состоят типы Dirty и Pure. Начнём с Pure. Там точно будет вся информация

необходимая нам для рисования картинки (ведь функция picture определена на Pure). Для рисования нам

необходимо знать положения всех шаров и их типы (они определяют цвет). На картинке мы будем показывать

разную статистику (данные о жизнях, бонусные очки). Также из типа Pure мы будем управлять созданием

шаров. Так мы приходим к типу:

data Pure = Pure

{ pureScores

:: Scores

, pureHero

:: HeroBall

, pureBalls

:: [Ball]

, pureStat

:: Stat

, pureCreation

:: Creation

}

Что нам нужно знать о шаре героя? Нам нужно его положение для отрисовки и модуль вектора скорости

(он понадобится нам при обновлении вектора скорости шара игрока):

data HeroBall = HeroBall

{ heroPos

:: H.Position

, heroVel

:: H.CpFloat

}

Для остальных шаров нам нужно знать только тип шара, его положение и идентификатор шара. По иден-

тификатору потом мы сможем понять какой шар удалить из грязных данных:

data Ball = Ball

{ ballType

:: BallType

, ballPos

:: H.Position

, ballId

:: Id

}

data BallType = Hero | Good | Bad | Bonus

deriving (Show, Eq, Enum)

type Id = Int

Статистика игры состоит из числа жизней и бонусных очков:

data Scores = Scores

{ scoresLives :: Int

, scoresBonus :: Int

}

Определяемся с типами | 301

Как будет происходить создание новых шаров? Если плохих шаров будет слишком много, то играть будет

не интересно, игрок слишком быстро проиграет. Если хороших шаров будет слишком много, то игроку также

быстро надоест. Будет очень легко. Нам необходимо поддерживать определённый баланс шаров. Создание

шаров будет происходить случайным образом через равные промежутки времени, но создание нового шара

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

то скорее всего мы создадим хороший шар и наоборот. Если общее число шаров велико, то мы не будем

усложнять игроку жизнь новыми шарами, дождёмся пока какие-нибудь шары не покинут пределы поля или

не будут уничтожены игроком. Эти рассуждения приводят нас к типам:

data Creation = Creation

{ creationStat

:: Stat

, creationGoalStat

:: Stat

, creationTick

:: Int

}

data Stat = Stat

{ goodCount

:: Int

, badCount

:: Int

, bonusCount

:: Int

}

data Freq = Freq

{ freqGood

:: Float

, freqBad

:: Float

, freqBonus

:: Float

}

Поле creationStat содержит текущее число шаров на поле, поле creationGoalStat – число шаров, к ко-

торому мы стремимся. Значение типа Freq содержит веса вероятностей создания нового шара определённого

типа. На каждом шаге мы будем прибавлять единицу к creationTiсk, как только оно достигнет определён-

ного значения мы попробуем создать новый шар.

Перейдём к грязным данным. Там мы будем хранить информацию, необходимую для обновления модели

в Hipmunk, и значение, в которое GLFW будет записывать состояние мыши, также мы будем следить за тем,

кто столкнулся с шаром игрока в данный момент:

data Dirty = Dirty

{ dirtyHero

:: Obj

, dirtyObjs

:: IxMap Obj

, dirtySpace

:: H.Space

, dirtyTouchVar :: Sensor H.Shape

, dirtyMouse

:: Sensor H.Position

}

data Obj = Obj

{ objType

:: BallType

, objShape

:: H.Shape

, objBody

:: H.Body

}

type Sensor a = IORef (Maybe a)

Особая структура IxMap отвечает за хранение значений вместе с индексами. Пока остановимся на самом

простом представлении:

type IxMap a = [(Id, a)]

20.4 Структура проекта

Наметим структуру проекта. У нас уже есть модуль Types. hs. Основной цикл игры будет описан в модуле

Loop. hs. Общие функции обновления состояния будут определены в World. hs, также у нас будет два модуля

отвечающие за обновление чистых и грязных данных – Pure. hs и Dirty. hs. Мы выделим отдельный модуль

для описания всех констант игры (Inits. hs). Так нам будет удобно настроить игру, когда мы закончим с

кодом. Отдельный модуль Utils будет содержать все функции общего назначения, преобразования между

типами OpenGL и Hipmunk.

302 | Глава 20: Императивное программирование

20.5 Детализируем функции обновления состояния игры

Начнём с восприятия:

module World where

import qualified Physics.Hipmunk as H

Читай продолжение на следующей странице
Добавить комментарии