diff --git a/config.json b/config.json index b669e58..ce33f4f 100644 --- a/config.json +++ b/config.json @@ -1,10 +1,10 @@ { - "version": "v0.0.1.4-2-g20fa78a", + "version": "v0.0.1.5", "title": "Alchemyst", "sizeX": 100, "sizeY": 47, "fpsLimit": 60, - "font": ".\/resources\/fonts-ttf\/LiberationMono-Bold.ttf", - "fontSize": "8x12", + "font": "./resources/fonts-ttf/LiberationMono-Bold.ttf", + "fontSize": "9x14", "verbosity": "debug" } \ No newline at end of file diff --git a/engine/fov/precomputed_shade/precomputed_shade.go b/engine/fov/precomputed_shade/precomputed_shade.go index e55e498..4dec910 100644 --- a/engine/fov/precomputed_shade/precomputed_shade.go +++ b/engine/fov/precomputed_shade/precomputed_shade.go @@ -78,6 +78,9 @@ Pre-Computed Visiblity Trees on RogueBasin Adam Milazzo's FOV Method Roundup where a similar method described as 'permissive' is detailed */ +const MIN_LIT_TO_BE_VISIBLE = 1 +const MIN_WALL_LIT_TO_BE_VISIBLE = 4 + var errNotFoundCell = errors.New("Cell not found") var errOutOfBounds = errors.New("Cell out of bounds") @@ -163,9 +166,9 @@ func (ps *precomputedShade) PrecomputeFovMap() { //Bresanham lines / Raycast var lineX, lineY float64 - for i := 0; i < 360; i++ { - dx := math.Sin(float64(i) / (float64(180) / math.Pi)) - dy := math.Cos(float64(i) / (float64(180) / math.Pi)) + for i := 0; i < 720; i++ { // 1/2 of angles + dx := math.Sin(float64(i) / (float64(360) / math.Pi)) //1/2 of angles + dy := math.Cos(float64(i) / (float64(360) / math.Pi)) lineX = 0 lineY = 0 @@ -204,11 +207,11 @@ func (ps *precomputedShade) recalc(level *gamemap.Level, initCoords types.Coords level.GetTile(initCoords).Visible = true - var fullShade = make([]byte, 360) + var fullShade = make([]byte, 720) // 1/2 of angles for i := range fullShade { fullShade[i] = 1 } - var emptyShade = make([]byte, 360) + var emptyShade = make([]byte, 720) // 1/2 of angles currentShade := emptyShade nextShade := emptyShade @@ -236,7 +239,11 @@ func (ps *precomputedShade) recalc(level *gamemap.Level, initCoords types.Coords //fmt.Printf("\n level coords: %v", lc) for _, angle := range cell.occludedAngles { - if level.GetTile(lc).BlocksSight { + if level.GetTile(lc).BlocksSight && ps.LightWalls { + if (nextShade[angle] == 0 && currentShade[angle] == 0) { + level.GetTile(lc).Visible = true + level.GetTile(lc).Explored = true + } nextShade[angle] = 1 } @@ -256,7 +263,7 @@ func (ps *precomputedShade) ComputeFov(level *gamemap.Level, initCoords types.Co for _, cell := range ps.CellList { //fmt.Printf("\n coords: %v, distance: %f, lit: %d", cell.Coords, cell.distance, cell.lit) cs, err := ps.toLevelCoords(level, initCoords, cell.Coords) - if cell.lit > 2 { + if cell.lit > MIN_LIT_TO_BE_VISIBLE { if err != nil { continue } @@ -265,21 +272,25 @@ func (ps *precomputedShade) ComputeFov(level *gamemap.Level, initCoords types.Co } //light walls, crutch - if level.GetTile(cs).BlocksSight && ps.LightWalls { - if cell.IsAdjacentTo(&types.Coords{0,0}) { - level.GetTile(cs).Visible = true - } else { - for _, maybeNb := range ps.CellList { - if //int(maybeNb.distance) == int(cell.distance-1) && - maybeNb.IsAdjacentTo(&cell.Coords) && - //(maybeNb.X == cell.X || maybeNb.Y == cell.Y) && - maybeNb.lit > 5 { //magic constant! - level.GetTile(cs).Visible = true - level.GetTile(cs).Explored = true - } - } - } - } + //if level.GetTile(cs).BlocksSight && ps.LightWalls { + // if cell.IsAdjacentTo(&types.Coords{0,0}) { + // level.GetTile(cs).Visible = true + // } else { + // maybeLit := false + // for _, maybeNb := range ps.CellList { + // if //int(maybeNb.distance) == int(cell.distance-1) && + // maybeNb.IsAdjacentTo(&cell.Coords) && + // (maybeNb.X == cell.X || maybeNb.Y == cell.Y) && + // maybeNb.lit > MIN_WALL_LIT_TO_BE_VISIBLE { //magic constant! + // maybeLit = true + // } + // } + // if maybeLit { + // level.GetTile(cs).Visible = true + // level.GetTile(cs).Explored = true + // } + // } + //} } } diff --git a/engine/screens/game.go b/engine/screens/game.go index 7405a31..49e9aa2 100644 --- a/engine/screens/game.go +++ b/engine/screens/game.go @@ -31,35 +31,35 @@ func (ts *GameScreen) Enter() { Print(1, ts.mw.H-2, "Press [color=white]?[/color] for help") } func (ts *GameScreen) Exit() { - ts.mw.GetLayer("base").ClearArea(1, ts.mw.H-2, 30, 1) + ts.mw.GetLayer("overlay").ClearArea(0, ts.mw.H-3, 30, 3) //remove what we dont need } func (ts *GameScreen) HandleInput(input string) { //ts.state.Do(func(){ switch input { - case "Up", "k", "8": + case "Up", "k", "KP_8": ts.walk(ts.state, 0, -1) break - case "Down", "j", "2": + case "Down", "j", "KP_2": ts.walk(ts.state, 0, 1) break - case "Left", "h", "4": + case "Left", "h", "KP_4": ts.walk(ts.state, -1, 0) break - case "Right", "l", "6": + case "Right", "l", "KP_6": ts.walk(ts.state, 1, 0) break - case "y", "7": + case "y", "KP_7": ts.walk(ts.state, -1, -1) break - case "u", "9": + case "u", "KP_9": ts.walk(ts.state, 1, -1) break - case "b", "1": + case "b", "KP_1": ts.walk(ts.state, -1, 1) break - case "n", "3": + case "n", "KP_3": ts.walk(ts.state, 1, 1) break case "Shift+/": diff --git a/.gopath b/story/choose_your_pill.md similarity index 100% rename from .gopath rename to story/choose_your_pill.md diff --git a/story/go_chans_for_game.md b/story/go_game_dos_and_donts.md similarity index 99% rename from story/go_chans_for_game.md rename to story/go_game_dos_and_donts.md index b278c97..d6455b1 100644 --- a/story/go_chans_for_game.md +++ b/story/go_game_dos_and_donts.md @@ -1,4 +1,6 @@ + + декодирование ввода в отдельном потоке Реализация FPS и троттлинг diff --git a/story/index.md b/story/index.md index f7c3f76..f97494c 100644 --- a/story/index.md +++ b/story/index.md @@ -1,5 +1,11 @@ -RLG и Golang - полезные советы +RLG и Golang - некоторые полезные советы === -1. [Установка и некоторые особенности работы](./linux_go_blt.md) связки BLT + Go на Linux -2. Реализация [некоторых возможностей](./go_chans_for_game.md) Go - chans, tickers, throttling -3. [Система типов](./static_types_vs_ecs.md) - нативная или ECS? \ No newline at end of file +0. [Выбираем инструменты](./choose_your_pill.md) +1. [Установка и некоторые особенности работы](linux_go_blt_install_quickstart.md) связки BLT + Go на Linux +2. Что [стоит и НЕ стоит](go_game_dos_and_donts.md) делать с возможностями Go - +chans, +tickers, +throttling, -closures +3. [Система типов](./static_types_vs_ecs.md) - нативная или ECS? На самом деле и то, и то + +Дополнения +--- + +1. Как [не делать лишнюю работу](./makefile_and_crosscompiling.md) и почему это **важно**. \ No newline at end of file diff --git a/story/linux_go_blt.md b/story/linux_go_blt.md deleted file mode 100644 index e32fd3e..0000000 --- a/story/linux_go_blt.md +++ /dev/null @@ -1,47 +0,0 @@ -Установка и работа с BLT на Linux -== - -Для Windows и Mac проблем с Go + BLT, насколько мне известно нет. -С Linux, которая моя основная рабочая ось - другая история, здесь вносят свой шарм особенности работы линкера. -Дело в том, что в составе BLT есть готовые биндинги для Go, НО! В Terminal/Include/Go по умолчанию -указаны такие флаги линкера CGO (стр 25) -```go -// #cgo LDFLAGS: -lBearLibTerminal -``` -Что подразумевает глобальную вивдимость библиотеки. Увы, пока пакета с BLT для распространенных дистрибутивов Linux нет. -Поэтому беде нужно помочь руками. Сначала вручную показать линтеру, что такая библиотека есть, и потом перезагрузить -кеш путей к библиотекам:: -```bash -$ sudo echo "/path/to/libbearterminal.so" > /etc/ld.so.conf.d/libbearterminal.conf && sudo ldconfig -``` - -Проблема тут в том, что эту же операцию придется проделать всем, кто захочет запустить ваше приложение с BLT. Вопреки -распространенному стереотипу - доля красноглазых пользователей Linux с каждым годом падает, и эта консольная магия для -большинства уже некомильфо. - -Способ второй, которым воспользовался я, намного проще для пользователя. -Редактируем файл с биндингами примерно следующим образом: -```go -// #cgo LDFLAGS: -L. -Wl,-rpath -Wl,./ -lBearLibTerminal -// #include -// #include -import "C" -``` -(знатоки С, простите, я этими флагами вообще -пользоваться не умею) - -Далее - собираем приложение с libtcod ```go build -o test```. - -Проверяем, что относительные пути записались в бинарник: -```bash -objdump -p test | grep RPATH -``` -Результат должен быть таким: -```bash -RPATH ./ -``` - -Ура! Теперь кладем libBearLibTerminal.so прямо в папку с main.go и запускам go run (или скомпилированный бинарник) прям -оттуда. Собранные таким образом бинарники будут искать библиотеку в той же папке, где находятся они сами. - -Теперь при дистрибуции приложения можно просто положить .so файл библиотеки рядом, и все будет работать! \ No newline at end of file diff --git a/story/linux_go_blt_install_quickstart.md b/story/linux_go_blt_install_quickstart.md new file mode 100644 index 0000000..0218b89 --- /dev/null +++ b/story/linux_go_blt_install_quickstart.md @@ -0,0 +1,153 @@ +Установка и работа с BLT на Linux +== + +Про Windows и Mac в контексте связки Go + BLT, я говорить не буду, поскольку не ел устриц. +С Linux, которая моя основная рабочая ось - другая история, здесь вносят свой шарм особенности работы линкера. +Дело в том, что BLT написана на C. Но! есть готовые биндинги для Go, НО! В Terminal/Include/Go по умолчанию +указаны такие флаги линкера CGO (стр 25) +```go +// #cgo LDFLAGS: -lBearLibTerminal +``` +Что подразумевает глобальную вивдимость библиотеки. Увы, пока пакета с BLT для +распространенных дистрибутивов Linux нет. +Поэтому беде нужно помочь руками. + +#####Первый метод + +Сначала вручную показать линтеру, что такая библиотека есть, и потом перезагрузить +кеш путей к библиотекам (пример для Ubuntu): +```bash +$ sudo echo "/path/to/libbearterminal.so" > /etc/ld.so.conf.d/libbearterminal.conf && sudo ldconfig +``` + +Проблема тут в том, что эту же операцию придется проделать всем, кто захочет запустить ваше приложение с BLT. Вопреки +распространенному стереотипу - доля красноглазых пользователей Linux с каждым годом падает, и эта консольная магия для +большинства уже некомильфо. + +#####Второй метод + +Способ второй, которым воспользовался я, с которым намного проще жить. +Редактируем файл с биндингами (BearLibTerminal.go) примерно следующим образом: +```go +// #cgo LDFLAGS: -L. -Wl,-rpath -Wl,./ -lBearLibTerminal +// #include +// #include +import "C" +``` +(знатоки С, простите если что не так, я этими флагами вообще +пользоваться не умею) + +Далее - собираем минимальное приложение с blt ```go build -o test```. + +Проверяем, что относительные пути записались в бинарник: +```bash +objdump -p test | grep RPATH +``` +Результат должен быть таким: +```bash +RPATH ./ +``` + +Ура! Теперь кладем libBearLibTerminal.so прямо в папку с main.go и запускам go run +(или скомпилированный бинарник) прям оттуда. Собранные таким образом бинарники будут +искать библиотеку в той же папке, где находятся они сами. + +Теперь при дистрибуции приложения можно просто положить .so файл библиотеки рядом, +и все будет работать! + +Горутины и многопоточность +--- + +Вторая кочка, на которой мне пришлось споткнуться - то то, что вызов любой своей +функции не из main thread BLT воспринимает крайне нервно, +и сыпет фатальными ошибками. Это неприятно, тк часто Го выбирают именно за +многопоточность из коробки. Но справедливости ради точно так же ведет себя и большинство +других библиотек связанных с рендером и вводом-выводом, тот же SDL например. Так что +воспринимайте это как милую особенность использования CGO. + +Лекарство тут ровно одно - вызывать сишные foreign functions из main thread. +Для реализации этого требования мне показалась полезной следующая конструкция +```go +package main + +import "runtime" + +... + + +// Рецепт чтобы убежать от [fatal] 'refresh' was not called from the main thread +// https://github.com/golang/go/wiki/LockOSThread +func init() { + runtime.LockOSThread() +} + +type GameState struct { + Mainfunc chan func() +} + +// do запускает функцию f в контексте main thread. +func (g *GameState) Do(f func()) { + done := make(chan struct{}, 1) + g.Mainfunc <- func() { + f() + f = nil //zero pointer в замыкание + done <- struct{}{} + } + <-done +} + + +var State = GameState{ + Mainfunc: make(chan func()), //блокирующий канал(!) +} +// И где-то в Main Loop делаем примерно так: +func MainLoop(state GameState) { + ... + //В этом select обработка ввода, рендер, пеерколючение состояний интерфейса итп + for f := range state.Mainfunc { + f() + }} +... +``` + +State - это обычный Value Object, экземпляр типа GameState. Я его использую как +контейнер для важных для игры данных - географии уровня, состояния объектов и мобов, +разных тикеров, каналов для рендера и ввода-вывода итп[1]. Так как он глобальный +(или просто передается всюду по аргументам), то именно в него встроен метод Do. + +Если нам скажем в пакете, где описывается некий предмет, надо нарисовать при его +поднятии какой-то супер-эффект на экране - мы поступаем вот так: + +```go +package item + +import "main" +import blt "some.repo.ru/user/bearlibterminal" + +var State main.GameState + +//функция скажем поднятия особенного предмета... +func (item *SpecialItem) Pickup() { + + .... + //выполняем строго в main thread + State.Do(func() { + renderSuperEffect() + }) +} +... +//тут собственно отрисовка эффекта +func renderSuperEffect() { + ... + blt.Layer(0) + blt.Print(x,y, "WAAAGH") + ... +} +``` + +Здесь renderSuperEffect - непосредственная реализация эффекта, doSuperEffect - +запихивает в очередь на выполнения в main thread эту самую реализацию. Которая с успехом +выполняется в main loop. В целом картина именно такая, но больше подробностей можно +найти по ссылкам в комментариях. + +[1]: Если такой контейнер аккуратно сериализовать (рекурсивно вместе со всем содержимым) и записать на диск... То потом можно его прочитать и десериализовать. Получив тем самым почти бесплатно Save / Load. \ No newline at end of file diff --git a/story/makefile_and_crosscompiling.md b/story/makefile_and_crosscompiling.md new file mode 100644 index 0000000..e3cfa87 --- /dev/null +++ b/story/makefile_and_crosscompiling.md @@ -0,0 +1,11 @@ +Автоматизация сборки и тестирование +=== + +- Почему это важно: мелочи сжирают кучу времени. Не позволяйте им это делать! +- настройка под Linux: все внешние либы собраны и включены в монорепо (дело вкуса) +- Go-специфичные вещи: glide, go mod +- Кросскомпиляция, CGO для Mac и Linux. CGO_ENABLED=1, mingw, локальная видимость библиотек +- Makefile и нафига он нужен +- Таргеты: Убираем бардак за собой - distclean, build +- Автоматическое тестирование, testify. Не ленитесь писать тесты! +- Деплой/публикация после сборки \ No newline at end of file diff --git a/story/static_types_vs_ecs.md b/story/static_types_vs_ecs.md index 39e6198..cd94f5a 100644 --- a/story/static_types_vs_ecs.md +++ b/story/static_types_vs_ecs.md @@ -2,8 +2,20 @@ Плюсы использования нативной системы типов +- Примитивы типа Coords, Rect +- Интерфейсы, type casting и переиспользование Blit + Минусы использования нативной системы типы -ECS - меняем бойлерплейт на относительное снижение связности +- Структура структуры (эм..) иммутабельна -Минутка рекламы gogue \ No newline at end of file +ECS - достигаем относительного снижения связности ценой чудовищного бойлерплейта + +- Делаем динамическую систему типов там и только там где нам надо +- Возможность физически впихнуть в кеш проца все актуальные данные +- Да, это много копипасты. Но мы используем статически типизированный язык, а не питон, что вы хотели? + +Минутка рекламы gogue + +- Толковый туториал, неплохой код +- Пользуйтесь и продвигайте \ No newline at end of file