Вдохновленный возможностями функционального программирования, в частности F#, и увидев на примере, что можно творить всего в несколько десяток строчек, решил реализовать простенькую версию самой сложной флеш-игры.
Получилось на скорую руку, но
Определим сначала, с какого типа объектами нам придется работать. Очевидно это будем мы сами в виде красного квадратика, желтые монетки и ненависные синие убийцы. Все эти классы будут реализовывать интерфейс
Paint будет рисовать на форме, а Recalc(time) — рассчитывать, где объект будет находится в момент времени time.
Все объекты будут находится в одном массиве
Самый простой объект, для работы с которым нужно знать лишь его текущие параметры (положение, размеры) и состояние (жив или умирает, так как умирать он будет постепенно).
Приступим к рисованию (упустив процесс умирания).
Сложная часть — реализовать Recalc. Сложность состоит в том, что бы не выходить за границы карты. Но об этом позже, так как мы еще не знаем, как задавать уровень.
Монеты. Задаются положением и скоростью вращения
В реализации класса ничего интересного, только нужно проверять, не пересекается ли она с RedSquare. Это можно делать в методе Recalc.
Для начала вытянем красный квадрат из массива
Не оптимальный метод, показываются возможности ФП. Создается множество, в которое добавляется объект, если он типа RedSquare и ничего — если любого другого. Так, как RedSquare только один — берем Seq.head
Далее идет стандартная задача пересечения круга и квадрата. Если пересекает — убиваем монету и добавляем в наш актив один поинт.
Самый интересный персонаж. Для его задания нужно много параметров —
координаты, радиус, скорость и замкнутый набор отрезков, по которым он будет двигаться. Отрезки задаются в виде векторов (dx, dy). Тоесть от текущего положения круг пойдет по первому отрезку, потом повернет на соответствующий второй вектор и так далее. После последнего вектора вернется на первый.
В данной реализации не имеется возможность двигать объект по кругу (разве сделать из него много-много-многоугольник и двигать по маленьким векторам).
Некоторые основные свойства класса
Реализуем ф-цию Recalc.
Как хорошо, что есть возможность взятие по модулю дробных чисел. Так, как путь круга циклический и зная время его прохождения, можно определить текущее положение
Для проверки пересечения с RedSquare, используем тот же метод, что и при реализации YellowSquare.
Естественным решением было задавать карту матрицей. Введем следующие обозначения
-1 — запрещенная зона
0 — свободная клетка
>0 — чекпоинты (зеленые области). На них можно сохраняться. Максимально число обозначает конец раунда (при наличии всех собраных монеток, естественно).
Да, все это хорошо, но пора бы определиться с тем, на чем и как это все рисовать.
Определим класс SmoothForm, наследованый от Form, и добавим несколько своих методов
x.Load загружает уровень, по карте, массиве объектов и количество монет, которые необходимо собрать, что бы завершить уровень.
x.Init в основном занимается тем, что вычисляет координаты точек сохранения для каждой зеленой области.
Собственно, осталось определить метод Paint и перехват нажатий клавиш
Для перехвата клавишь, как оказалось, ничего сложного делать не надо
Аналогично для form.KeyUp
Что-то похоже…
Осталось научится загружать уровень из файлов. Для этого напишем функцию, принимающую путь к файлу как параметр и возвращающую параметры уровня. В файле будут идти
Так как эта функция реализована после всех классов — нужно ее делегат добавить в форму
(dd.Invoke) выполняет функцию с указанными параметрами.
Конечно данная реализация не является гибкой или оптимальной. Код и сами уровни находятся в состоянии доработки. Буду рад выслушать комментарии и пожелания.
UPD. Код+ехе+2 левела
Получилось на скорую руку, но
Основные объекты
Определим сначала, с какого типа объектами нам придется работать. Очевидно это будем мы сами в виде красного квадратика, желтые монетки и ненависные синие убийцы. Все эти классы будут реализовывать интерфейс
type IPaintObject =
abstract Paint : Graphics -> unit
abstract Recalc : float -> unit
Paint будет рисовать на форме, а Recalc(time) — рассчитывать, где объект будет находится в момент времени time.
Все объекты будут находится в одном массиве
let po = new ResizeArray<IPaintObject>()
RedSquare
Самый простой объект, для работы с которым нужно знать лишь его текущие параметры (положение, размеры) и состояние (жив или умирает, так как умирать он будет постепенно).
type RedSquare(xx:int, yy:int, ww:int, hh:int, speed:int) =
...
member rs.X with get() = int xCoord and set(v) = (xCoord <- v)
member rs.Y with get() = int yCoord and set(v) = (yCoord <- v)
member rs.W with get() = width and set(v) = (width <- v)
member rs.H with get() = height and set(v) = (height <- v)
member rs.Got with get() = gather // сколько монеток съедено
member rs.isDying with get() = (dying>0)
member rs.Speed = speed
Приступим к рисованию (упустив процесс умирания).
interface IPaintObject with
member obj.Paint(g) =
let rect =
match (dying) with
| 0 -> Rectangle(x=int xCoord-width/2, y=int yCoord-height/2, width=width, height=height)
...
g.FillRectangle(Brushes.Red, rect)
g.DrawRectangle(new Pen(Color.Black, float32 2), rect)
Сложная часть — реализовать Recalc. Сложность состоит в том, что бы не выходить за границы карты. Но об этом позже, так как мы еще не знаем, как задавать уровень.
YellowCircle
Монеты. Задаются положением и скоростью вращения
type YellowCircle(xx:int, yy:int, rr:int, tr:float) =
...
В реализации класса ничего интересного, только нужно проверять, не пересекается ли она с RedSquare. Это можно делать в методе Recalc.
Для начала вытянем красный квадрат из массива
let rs = seq { for obj in po do
match obj with
| :? RedSquare as p ->
yield p
| _ -> yield! Seq.empty
} |> Seq.head
Не оптимальный метод, показываются возможности ФП. Создается множество, в которое добавляется объект, если он типа RedSquare и ничего — если любого другого. Так, как RedSquare только один — берем Seq.head
Далее идет стандартная задача пересечения круга и квадрата. Если пересекает — убиваем монету и добавляем в наш актив один поинт.
if (isIntersects xx yy rr (rs.X-rs.W/2) (rs.Y-rs.H/2) (rs.W) (rs.H)) then
yc.Take()
rs.Add()
BlueCircle
Самый интересный персонаж. Для его задания нужно много параметров —
type BlueCircle(xx:int, yy:int, rr:int, speed:int, segments:(int*int)[]) =
координаты, радиус, скорость и замкнутый набор отрезков, по которым он будет двигаться. Отрезки задаются в виде векторов (dx, dy). Тоесть от текущего положения круг пойдет по первому отрезку, потом повернет на соответствующий второй вектор и так далее. После последнего вектора вернется на первый.
В данной реализации не имеется возможность двигать объект по кругу (разве сделать из него много-много-многоугольник и двигать по маленьким векторам).
Некоторые основные свойства класса
member bc.Stable with get() = (bc.TotalDist < 1e-8) // стабильный или динамический
member bc.Speed with get() = float speed
member bc.Dists = segments |> Array.map(fun (dx, dy) -> Math.Sqrt(float(dx*dx+dy*dy))) // массив расстояний
member bc.TotalDist = bc.Dists |> Array.sum
member bc.TotalTime = bc.TotalDist/bc.Speed
Реализуем ф-цию Recalc.
Как хорошо, что есть возможность взятие по модулю дробных чисел. Так, как путь круга циклический и зная время его прохождения, можно определить текущее положение
member bc.Recalc(tt) =
// если стабильный - нечего высчитывать, иначе
if (bc.Stable=false) then
let mutable t1 = tt%bc.TotalTime
let mutable ind = 0
X <- xx
Y <- yy
// зная скорость и время - проходим сегменты, пока не найдем текущий
while (ind<len-1 && t1*bc.Speed>=bc.Dists.[ind]) do
X <- X + (fst segments.[ind])
Y <- Y + (snd segments.[ind])
t1 <- t1-bc.Dists.[ind]/bc.Speed
ind <- ind+1
// двигаем на вектор
let (dx, dy) = (((float (fst segments.[ind]))/(bc.Dists.[ind])),
((float (snd segments.[ind]))/(bc.Dists.[ind])))
X <- X + int (dx*t1*bc.Speed)
Y <- Y + int (dy*t1*bc.Speed)
Для проверки пересечения с RedSquare, используем тот же метод, что и при реализации YellowSquare.
Карта
Естественным решением было задавать карту матрицей. Введем следующие обозначения
-1 — запрещенная зона
0 — свободная клетка
>0 — чекпоинты (зеленые области). На них можно сохраняться. Максимально число обозначает конец раунда (при наличии всех собраных монеток, естественно).
Форма
Да, все это хорошо, но пора бы определиться с тем, на чем и как это все рисовать.
Определим класс SmoothForm, наследованый от Form, и добавим несколько своих методов
type SmoothForm(dx:int, dy:int, _path:string) as x =
inherit Form()
do x.DoubleBuffered <- true
...
let mutable Map = null
member x.Load(_map:int[][], obj, _need) =
Map <- _map
po.Clear()
for o in obj do
po.Add o
need <- _need
x.Init()
x.Load загружает уровень, по карте, массиве объектов и количество монет, которые необходимо собрать, что бы завершить уровень.
x.Init в основном занимается тем, что вычисляет координаты точек сохранения для каждой зеленой области.
Собственно, осталось определить метод Paint и перехват нажатий клавиш
let form = new SmoothForm(Text="F# The world hardest game", Visible=true, TopMost=true,Width=.../*куча параметров*/)
form.Paint.Add(fun arg ->
let g = arg.Graphics
for i=0 to form.rows-1 do
for j=0 to form.cols-1 do
match (form.map.[i].[j], (i+j)%2) with
// запрещенная зона
| (-1, _) -> g.FillRectangle(Brushes.DarkViolet, j*form.DX, i*form.DY, form.DX, form.DY)
// пустая клетка
| ( 0, 0) -> g.FillRectangle(Brushes.White, j*form.DX, i*form.DY, form.DX, form.DY)
// пустая клетка
| ( 0, 1) -> g.FillRectangle(Brushes.LightGray, j*form.DX, i*form.DY, form.DX, form.DY)
// пустая клетка
| ( p, _) when p>0 -> g.FillRectangle(Brushes.LightGreen, j*form.DX, i*form.DY+1, form.DX, form.DY)
// граница
if (i>0 && (form.map.[i].[j]>=0 && form.map.[i-1].[j]<0
|| form.map.[i].[j]<0 && form.map.[i-1].[j]>=0)) then
g.DrawLine(new Pen(Color.Black, float32 2), j*form.DX, i*form.DY, (j+1)*form.DX, i*form.DY)
// граница
if (j>0 && (form.map.[i].[j]>=0 && form.map.[i].[j-1]<0
|| form.map.[i].[j]<0 && form.map.[i].[j-1]>=0)) then
g.DrawLine(new Pen(Color.Black, float32 2), j*form.DX, i*form.DY, j*form.DX, (i+1)*form.DY)
for obj in po do
// пересчитываем местоположения и рисуем
obj.Recalc((DateTime.Now-SS).TotalSeconds)
obj.Paint(g)
async { do! Async.Sleep(10) // спим 10мсек
form.Invalidate() } |> Async.Start
)
Для перехвата клавишь, как оказалось, ничего сложного делать не надо
form.KeyDown
// из всех нажатий оставим нажатия стрелочками
|> Event.filter(fun args -> (args.KeyValue >= 37) && (args.KeyValue <= 40))
|> Event.add (fun args ->
match (args.KeyCode) with
| Keys.Down -> form.Down <- 1
| Keys.Left -> form.Left <- 1
| Keys.Right -> form.Right <- 1
| Keys.Up -> form.Up <- 1
)
Аналогично для form.KeyUp
Что-то похоже…
Осталось научится загружать уровень из файлов. Для этого напишем функцию, принимающую путь к файлу как параметр и возвращающую параметры уровня. В файле будут идти
- Размеры карты
- Карта
- Количество BlueCircle
- Параметры каждого из них
- Количество YellowCircle
- Параметры каждого из них
- Координаты, размеры и скорость RedSquare
let LoadLevel _path =
let pp = new ResizeArray<IPaintObject>()
let data = File.ReadAllLines(_path) |> Array.toSeq;
let L1 = data
|> Seq.skip 1
|> Seq.take n
|> Seq.toArray
|> Array.map(fun x -> x.Split([|' '|]) |> Array.filter(fun x -> Int32.TryParse(x, ref tmp)) |> Array.map(fun x -> Int32.Parse(x)))
...
Так как эта функция реализована после всех классов — нужно ее делегат добавить в форму
type DelegateLoad = delegate of (string) -> (int[][]*ResizeArray<IPaintObject>*int)
type SmoothForm(dx:int, dy:int, _path:string) as x =
...
let mutable (dd:DelegateLoad) = null
...
member x.LoadNext() =
currLevel <- currLevel + 1
let pathToLevel = pathToFolder+"\\"+"L"+currLevel.ToString()+".txt"
if (File.Exists(pathToLevel) = false) then
complete <- 1
else
x.Load(dd.Invoke(pathToLevel))
x.Invalidate()
(dd.Invoke) выполняет функцию с указанными параметрами.
Заключение
Конечно данная реализация не является гибкой или оптимальной. Код и сами уровни находятся в состоянии доработки. Буду рад выслушать комментарии и пожелания.
UPD. Код+ехе+2 левела