Merge branch 'master' of lab.zaar.be:thefish/alchemyst-go

This commit is contained in:
thefish 2019-11-08 22:10:01 +03:00
commit 4ba69bfe75
10 changed files with 235 additions and 87 deletions

View File

@ -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"
}

View File

@ -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
// }
// }
//}
}
}

View File

@ -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+/":

View File

@ -1,4 +1,6 @@
декодирование ввода в отдельном потоке
Реализация FPS и троттлинг

View File

@ -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?
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) и почему это **важно**.

View File

@ -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 <stdlib.h>
// #include <BearLibTerminal.h>
import "C"
```
(знатоки С, простите, я этими флагами вообще
пользоваться не умею)
Далее - собираем приложение с libtcod ```go build -o test```.
Проверяем, что относительные пути записались в бинарник:
```bash
objdump -p test | grep RPATH
```
Результат должен быть таким:
```bash
RPATH ./
```
Ура! Теперь кладем libBearLibTerminal.so прямо в папку с main.go и запускам go run (или скомпилированный бинарник) прям
оттуда. Собранные таким образом бинарники будут искать библиотеку в той же папке, где находятся они сами.
Теперь при дистрибуции приложения можно просто положить .so файл библиотеки рядом, и все будет работать!

View File

@ -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 <stdlib.h>
// #include <BearLibTerminal.h>
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.

View File

@ -0,0 +1,11 @@
Автоматизация сборки и тестирование
===
- Почему это важно: мелочи сжирают кучу времени. Не позволяйте им это делать!
- настройка под Linux: все внешние либы собраны и включены в монорепо (дело вкуса)
- Go-специфичные вещи: glide, go mod
- Кросскомпиляция, CGO для Mac и Linux. CGO_ENABLED=1, mingw, локальная видимость библиотек
- Makefile и нафига он нужен
- Таргеты: Убираем бардак за собой - distclean, build
- Автоматическое тестирование, testify. Не ленитесь писать тесты!
- Деплой/публикация после сборки

View File

@ -2,8 +2,20 @@
Плюсы использования нативной системы типов
- Примитивы типа Coords, Rect
- Интерфейсы, type casting и переиспользование Blit
Минусы использования нативной системы типы
ECS - меняем бойлерплейт на относительное снижение связности
- Структура структуры (эм..) иммутабельна
Минутка рекламы gogue
ECS - достигаем относительного снижения связности ценой чудовищного бойлерплейта
- Делаем динамическую систему типов там и только там где нам надо
- Возможность физически впихнуть в кеш проца все актуальные данные
- Да, это много копипасты. Но мы используем статически типизированный язык, а не питон, что вы хотели?
Минутка рекламы gogue
- Толковый туториал, неплохой код
- Пользуйтесь и продвигайте