Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add selector #12

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open

Add selector #12

wants to merge 13 commits into from

Conversation

aled93
Copy link
Contributor

@aled93 aled93 commented Feb 5, 2025

Code without selector:

type animationSpriteMatrixController struct{}

func (s *animationSpriteMatrixController) Init(world *ecs2.World) {}
func (s *animationSpriteMatrixController) Update(world *ecs2.World) {
	animationPlayers := components.AnimationPlayerService.GetManager(world)
	animationStates := components.AnimationStateService.GetManager(world)
	spriteMatrixes := components.SpriteMatrixService.GetManager(world)

	animationPlayers.AllParallel(func(e ecs2.Entity, animationPlayer *components.AnimationPlayer) bool {
		spriteMatrix := spriteMatrixes.Get(e)
		if spriteMatrix == nil {
			return true
		}

		animationStatePtr := animationStates.Get(e)
		if animationStatePtr == nil {
			return true
		}
		animationState := *animationStatePtr

		if animationPlayer.State == animationState && animationPlayer.IsInitialized == true {
			return true
		}

		currentAnimation := spriteMatrix.Animations[animationState]

		animationPlayer.First = 0
		animationPlayer.Current = 0
		animationPlayer.Last = currentAnimation.NumOfFrames - 1
		animationPlayer.Loop = currentAnimation.Loop
		animationPlayer.Vertical = currentAnimation.Vertical
		animationPlayer.FrameDuration = time.Second / time.Duration(spriteMatrix.FPS)
		animationPlayer.State = animationState
		animationPlayer.Speed = 1
		animationPlayer.IsInitialized = true

		return true
	})
}
func (s *animationSpriteMatrixController) FixedUpdate(world *ecs2.World) {}
func (s *animationSpriteMatrixController) Destroy(world *ecs2.World)     {}

and with selector:

type animationSpriteMatrixController struct {
	selector ecs.Selector[struct {
		Player *components.AnimationPlayer
		State  *components.AnimationState
		Matrix *components.SpriteMatrix
	}]
}

func (s *animationSpriteMatrixController) Init(world *ecs.World) {
	world.RegisterSelector(&s.selector)
}
func (s *animationSpriteMatrixController) Update(world *ecs.World) {
	for c := range s.selector.All() {
		if c.Player.State == *c.State && c.Player.IsInitialized == true {
			continue
		}

		currentAnimation := c.Matrix.Animations[*c.State]

		c.Player.First = 0
		c.Player.Current = 0
		c.Player.Last = currentAnimation.NumOfFrames - 1
		c.Player.Loop = currentAnimation.Loop
		c.Player.Vertical = currentAnimation.Vertical
		c.Player.FrameDuration = time.Second / time.Duration(c.Matrix.FPS)
		c.Player.State = *c.State
		c.Player.Speed = 1
		c.Player.IsInitialized = true
	}
}
func (s *animationSpriteMatrixController) FixedUpdate(world *ecs.World) {}
func (s *animationSpriteMatrixController) Destroy(world *ecs.World)     {}

Need performance tests

@milanjrodd
Copy link
Collaborator

milanjrodd commented Feb 9, 2025

За перформанс тест есть мысли?

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

@aled93
Copy link
Contributor Author

aled93 commented Feb 9, 2025

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

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

За перформанс тест есть мысли?

Пока всё ещё не тестировал, но рефлексия это всегда накладно, особено если в главном цикле используется как здесь, но, имхо, удобство, которое можно сделать с его помощью, перевешывает это. Есть идея пофиксить это не ломая интерфейс с помощью необязательной кодогенерации которую можно запускать для релизного билда, а тестовые будут работать на рефлексии для быстрого запуска, т.к. го из коробки не умеет каждую компиляцию запускать кодогенератор. Этот кодогенератор будет генерировать специализированный метод pullComponentInstances без рефлексии.

@milanjrodd
Copy link
Collaborator

milanjrodd commented Feb 9, 2025

селектор итерирует по уже известным ему сущностям

Проблема в том, что это я так понял работает через пополнение лукап таблицы с entityid, а данные остаются в тех же плотных компонентах, верно? Если так, то это антипатерн и 100% нарушает идею ecs о плотности данных для меньших кэш миссов.

которые ему подкидывает\забирает сам мир при изменении в наборе компонентов каждой сущности

Если не регистрировать селектор, то подкидывания и рефлексии соответственно не будет, верно?

Ну и рефлексия в update цикле движка это смЭрть

@milanjrodd
Copy link
Collaborator

На счёт удобства есть другой вариант. Сделать утилити функцию (вероятно несколько и generic) которая будет принимать компоненты и в том порядке что их приняла ходить по ним и выполнять работу.

@aled93
Copy link
Contributor Author

aled93 commented Feb 9, 2025

На счёт удобства есть другой вариант. Сделать утилити функцию (вероятно несколько и generic) которая будет принимать компоненты и в том порядке что их приняла ходить по ним и выполнять работу.

У меня был вариант без рефлексии, но дженерик селектор, там 8 селекторов Selector2[T1 any, T2 any], Selector3[T1 any, T2 any, T3 any]... вариадик параметров типа в го, увы нет, приходится так. Но зато без рефлексии, я чот его забраковал :) тебе он бы явно больше понравился, там только в итераторе надо в том же порядке вытягивать инстансы компонентов:

selector ecs.Selector3[components.Position, components.InputIntent, components.AnimationState]
...
world.RegisterSelector(&s.selector)
...
for c := range s.selector.All() {
  // мне вот это не понравилось, вытягивать по имени удобнее, но да, может то что здесь рефлексии нет намного лучше
  var pos := c.C1
  var inputIntent := c.C2
  var animState := c.C3
}

Протестил 10к ботов-миланчиков pprof'ом:
изображение
изображение
В кратце где-то на 10-15% медленнее

@aled93
Copy link
Contributor Author

aled93 commented Feb 9, 2025

Проблема в том, что это я так понял работает через пополнение лукап таблицы с entityid, а данные остаются в тех же плотных компонентах, верно? Если так, то это 100% нарушает идею ecs о плотности данных для меньших кэш миссов.

Да, пока селектор это просто селектор. Хранилища теже самые. Ну и в играх без использования архетипов тоже же нужно обрабатывать несколько компонентов в каждой сущности.

Я ещё не углублялся в подробности реализации архетипного хранилища, пока у меня сомнения что это панацея решающая все кэш миссы. Например набор компонентов у игрока: Position, Rotation, Scale, SpriteMatrix, Tint, AnimationPlayer, AnimationState, Mirrored, InputIntent, это должен быть отдельный архетип? Или, например, Position, Rotation, Scale должен быть отдельным вложенным архетипом? Ведь по этой троице явно будет много итераций и вне систем связанных с игроком, а если один большой архетип на каждый уникальный набор компонентов будет то тогда и итерация по, например, только Position будет с большими гапами между инстансами компонента.
Возможно архетипы должны строится от того, какие компоненты системы запрашивают, а не от созданных сущностей.

@aled93
Copy link
Contributor Author

aled93 commented Feb 10, 2025

В отдельный бранч закоммитил этот селектор: aled93@6123c2d в итераторе у них никакого рефлекта aled93@6123c2d#diff-2e0c4c1cbdcf5d13e3c679bd70913e595e8843a7871e352df9f10a7d95486a41R289

func (s *Selector4[T1, T2, T3, T4]) All() iter.Seq[Selector4Elements[T1, T2, T3, T4]] {
	return func(yield func(Selector4Elements[T1, T2, T3, T4]) bool) {
		for entId := range s.matchedEnts.Keys() {
			var elems Selector4Elements[T1, T2, T3, T4]
			elems.C1 = s.t1Manager.Get(entId)
			elems.C2 = s.t2Manager.Get(entId)
			elems.C3 = s.t3Manager.Get(entId)
			elems.C4 = s.t4Manager.Get(entId)
			if !yield(elems) {
				break
			}
		}
	}
}

Перформанс у него повыше чем у нынешнего метода итерации по нескольким компонентам (см. скрины, первый с селектором без рефлекта, второй вообще без селекторов).

изображение
изображение
10 тысяч миланчиков готовы, ещё миллион на подходе

Так что теперь это не только удобство, но и +скорость :) отпишись доволен ли ты теперь, я смогу смержить в пул реквестный этот бранч через пару часов.

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

Попробовал в главном цикле заменить рефлект на unsafe - всё равно чуть-чуть медленее:

~ fps
no selectors 51-52 fps
no reflect 52-53 fps
reflect+unsafe 49-50 fps

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants