Делать --- #### декодирование ввода в отдельном потоке Этот пункт исключительно только ради удобства. По умолчанию в BLT ввод отдается в виде int-ов, мне гораздо удобнее получать ввод в виде "<модификатор(ы)+><имя клавиши>", например "Ctrl+Space". Для этого пришлось написать map[int]string и пеервести все BLT-токены для клавиатуры в строки, а также написать простенький обработчик: ```go package main func decodeInput(ctx util.ClientCtx) { var exit = false for !exit { select { case keycode := <-State.RawInput: if keycode == blt.TK_NONE { continue } if keycode == blt.TK_CLOSE { State.Exit <- struct{}{} return } var pressed = "" var isModifier, _ = util.IntInSlice(keycode, modifiers) if !isModifier { pressed = ui.Scancodemap[keycode] if blt.Check(blt.TK_SHIFT) != 0 { pressed = "Shift+" + pressed } if blt.Check(blt.TK_ALT) != 0 { pressed = "Alt+" + pressed } if blt.Check(blt.TK_CONTROL) != 0 { pressed = "Ctrl+" + pressed } //global hotkeys switch pressed { //case "Ctrl+q", "Escape": case "Ctrl+q": State.Exit <- struct{}{} exit = true return default: if pressed != "" { State.Input <- pressed } } } } } } ``` и в main функции обустроить всё для ее работы: ```go var State = gamestate.GameState{ Exit: make(chan struct{}, 1), Input: make(chan string, 1), RawInput: make(chan int, 1), } ... func main() { ... go decodeInput() ... var exit = false for !exit { select { case State.RawInput <- readKeyCode(): break case pressed := <-State.Input: screenMgr.CurrentScreen.HandleInput(pressed) break case <-State.Exit: mainCtx.Logger().Warn().Msg("quitting NOW") exit = true break ... } ... } func readKeyCode() int { if !blt.HasInput() { return blt.TK_NONE } return blt.Read() } ``` Теперь как только что-то приезжает к канал State.Input - просто передаем это в обработчик ввода текущего экрана. И пользуемся удобными обозначениями клавиш. #### Реализация FPS и троттлинг FPS в случае использования скажем SDL напрямую может уехать за несколько сотен. Это не лучшим способом скажется на утилизации CPU. Иногда нужен отдельный FPS для некоторых видов анимации (в моем случае есть слегка анимиррованные тайлы). реализуется это достаточно просто: ```go type TerrainRenderer { ... animateTiles *time.Ticker //поле для тикера ... } func NewTerrainRenderer() *TerrainRenderer { t := &TerrainRenderer{} ... t.animateTiles = time.NewTicker(time.Second / 12) return t } ... var redraw = true ... //Это запускается в main thread ДО main loop в отдельном потоке - go terrainRenderer.Listen() func (tr *TerrainRenderer) Listen(state gamestate.GameState) { for { select { case <-state.FovRecompute: //глобальное состояние говорит что видимость изменилась fovRecompute = true case <-state.Redraw: // или что просто надо совежить картинку redraw = true case <-tr.animateTiles.C: // а вот внутренний тикер говорит нам что пора играть следующий кадр анимации redraw = true } } } ... //А это собственно отрисовка ландшафта, вызывается в main thread func (tr *TerrainRender) Render() { if redraw { redraw = false //рисуем ландшафт на карте ... } ... } ``` TODO: троттлинг #### Каналы состояний и их Listеner-ы Ну вобщем-то из кода выше видно, как это работает. Для состояния движка ок, для состояния игры - кмк больше подойдет обычная или timed queue. Не делать == - Замыкания (не зная точно, где и как вы их уничтожите). Просто наберите в гугле golang closure memleak - reflect в main loop. Лишь **только** выкинув рефлкесию и больше ничего не делая - я снизил потребление CPU приложением **вдвое**. Это удобная штука, не спорю, но пользоваться ей надо при загрузке ресурсов, при сохранении/загрузке состояния приложения - т.е. при разовых операциях. Как оказалось, она _очень_ дорогая по CPU. Кто пользуется ей в main loop, ORM и прочих нагруженных местах - да будет предан анафеме. Обратить внимание === При казалось бы передаче по значению слайсы - всё равно передаются по ссылке! (пример с комнатами и выходами)