352 lines
		
	
	
	
		
			8.7 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			352 lines
		
	
	
	
		
			8.7 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| #include <ecs/registry.hpp>
 | |
| #include <ranges>
 | |
| #include <test/expects.hpp>
 | |
| #include <test/test.hpp>
 | |
| 
 | |
| using lt::test::Case;
 | |
| using lt::test::expect_unreachable;
 | |
| using lt::test::Suite;
 | |
| 
 | |
| using lt::test::expect_eq;
 | |
| using lt::test::expect_ne;
 | |
| 
 | |
| using lt::test::expect_false;
 | |
| using lt::test::expect_true;
 | |
| 
 | |
| using lt::ecs::EntityId;
 | |
| using lt::ecs::Registry;
 | |
| 
 | |
| struct Component
 | |
| {
 | |
| 	int m_int {};
 | |
| 	std::string m_string;
 | |
| 
 | |
| 	[[nodiscard]] friend auto operator==(const Component &lhs, const Component &rhs) -> bool
 | |
| 	{
 | |
| 		return lhs.m_int == rhs.m_int && lhs.m_string == rhs.m_string;
 | |
| 	}
 | |
| };
 | |
| template<>
 | |
| struct std::formatter<Component>
 | |
| {
 | |
| 	constexpr auto parse(std::format_parse_context &context)
 | |
| 	{
 | |
| 		return context.begin();
 | |
| 	}
 | |
| 
 | |
| 	auto format(const Component &val, std::format_context &context) const
 | |
| 	{
 | |
| 		return std::format_to(context.out(), "{}, {}", val.m_int, val.m_string);
 | |
| 	}
 | |
| };
 | |
| 
 | |
| struct Component_B
 | |
| {
 | |
| 	float m_float {};
 | |
| 
 | |
| 	[[nodiscard]] friend auto operator==(const Component_B lhs, const Component_B &rhs) -> bool
 | |
| 	{
 | |
| 		return lhs.m_float == rhs.m_float;
 | |
| 	}
 | |
| };
 | |
| template<>
 | |
| struct std::formatter<Component_B>
 | |
| {
 | |
| 	constexpr auto parse(std::format_parse_context &context)
 | |
| 	{
 | |
| 		return context.begin();
 | |
| 	}
 | |
| 
 | |
| 	auto format(const Component_B &val, std::format_context &context) const
 | |
| 	{
 | |
| 		return std::format_to(context.out(), "{}", val.m_float);
 | |
| 	}
 | |
| };
 | |
| 
 | |
| Suite raii = "raii"_suite = [] {
 | |
| 	Case { "happy path won't throw" } = [] {
 | |
| 		std::ignore = Registry {};
 | |
| 	};
 | |
| 
 | |
| 	Case { "many won't freeze/throw" } = [] {
 | |
| 		for (auto idx : std::views::iota(0, 100'000))
 | |
| 		{
 | |
| 			std::ignore = Registry {};
 | |
| 		}
 | |
| 	};
 | |
| 
 | |
| 	Case { "unhappy path throws" } = [] {
 | |
| 	};
 | |
| 
 | |
| 	Case { "post construct has correct state" } = [] {
 | |
| 		auto registry = Registry {};
 | |
| 		expect_eq(registry.get_entity_count(), 0);
 | |
| 	};
 | |
| };
 | |
| 
 | |
| Suite entity_raii = "entity_raii"_suite = [] {
 | |
| 	Case { "create_entity returns unique values" } = [] {
 | |
| 		auto registry = Registry {};
 | |
| 		auto set = std::unordered_set<EntityId> {};
 | |
| 
 | |
| 		for (auto idx : std::views::iota(0, 10'000))
 | |
| 		{
 | |
| 			auto entity = registry.create_entity();
 | |
| 			expect_false(set.contains(entity));
 | |
| 
 | |
| 			set.insert(entity);
 | |
| 			expect_eq(set.size(), idx + 1);
 | |
| 		}
 | |
| 	};
 | |
| 
 | |
| 	Case { "post create/destroy_entity has correct state" } = [] {
 | |
| 		auto registry = Registry {};
 | |
| 
 | |
| 		auto entities = std::vector<EntityId> {};
 | |
| 		for (auto idx : std::views::iota(0, 10'000))
 | |
| 		{
 | |
| 			entities.emplace_back(registry.create_entity());
 | |
| 			expect_eq(registry.get_entity_count(), idx + 1);
 | |
| 		}
 | |
| 
 | |
| 		for (auto idx : std::views::iota(0, 10'000))
 | |
| 		{
 | |
| 			auto entity = entities.back();
 | |
| 			registry.destroy_entity(entity);
 | |
| 
 | |
| 			entities.pop_back();
 | |
| 			expect_eq(registry.get_entity_count(), 10'000 - (idx + 1));
 | |
| 		}
 | |
| 	};
 | |
| };
 | |
| 
 | |
| Suite component_raii = "component_raii"_suite = [] {
 | |
| 	Case { "add has correct state" } = [] {
 | |
| 		auto registry = Registry {};
 | |
| 		for (auto idx : std::views::iota(0, 100'000))
 | |
| 		{
 | |
| 			auto entity = registry.create_entity();
 | |
| 			auto &component = registry.add<Component>(
 | |
| 			    entity,
 | |
| 			    { .m_int = idx, .m_string = std::to_string(idx) }
 | |
| 			);
 | |
| 
 | |
| 			expect_eq(component.m_int, idx);
 | |
| 			expect_eq(component.m_string, std::to_string(idx));
 | |
| 		}
 | |
| 	};
 | |
| 
 | |
| 	Case { "remove has correct state" } = [] {
 | |
| 		auto registry = Registry {};
 | |
| 		for (auto idx : std::views::iota(0, 100'000))
 | |
| 		{
 | |
| 			auto entity = registry.create_entity();
 | |
| 			auto &component = registry.add<Component>(
 | |
| 			    entity,
 | |
| 			    { .m_int = idx, .m_string = std::to_string(idx) }
 | |
| 			);
 | |
| 
 | |
| 			expect_eq(component.m_int, idx);
 | |
| 			expect_eq(component.m_string, std::to_string(idx));
 | |
| 		}
 | |
| 	};
 | |
| };
 | |
| 
 | |
| Suite callbacks = "callbacks"_suite = [] {
 | |
| 	Case { "connecting on_construct/on_destruct won't throw" } = [] {
 | |
| 		auto registry = Registry {};
 | |
| 		registry.connect_on_construct<Component>([&](Registry &, EntityId) {});
 | |
| 		registry.connect_on_destruct<Component>([&](Registry &, EntityId) {});
 | |
| 	};
 | |
| 
 | |
| 	Case { "on_construct/on_destruct won't get called on unrelated component" } = [] {
 | |
| 		auto registry = Registry {};
 | |
| 		registry.connect_on_construct<Component>([&](Registry &, EntityId) {
 | |
| 			expect_unreachable();
 | |
| 		});
 | |
| 		registry.connect_on_destruct<Component>([&](Registry &, EntityId) {
 | |
| 			expect_unreachable();
 | |
| 		});
 | |
| 
 | |
| 		for (auto idx : std::views::iota(0, 100'000))
 | |
| 		{
 | |
| 			registry.add<Component_B>(registry.create_entity(), {});
 | |
| 		}
 | |
| 	};
 | |
| 
 | |
| 	Case { "on_construct/on_destruct gets called" } = [] {
 | |
| 		auto registry = Registry {};
 | |
| 		auto all_entities = std::vector<EntityId> {};
 | |
| 		auto on_construct_called = std::vector<EntityId> {};
 | |
| 		auto on_destruct_called = std::vector<EntityId> {};
 | |
| 
 | |
| 		registry.connect_on_construct<Component>([&](Registry &, EntityId entity) {
 | |
| 			on_construct_called.emplace_back(entity);
 | |
| 		});
 | |
| 		registry.connect_on_destruct<Component>([&](Registry &, EntityId entity) {
 | |
| 			on_destruct_called.emplace_back(entity);
 | |
| 		});
 | |
| 
 | |
| 		expect_true(on_construct_called.empty());
 | |
| 		expect_true(on_destruct_called.empty());
 | |
| 		for (auto idx : std::views::iota(0, 100'000))
 | |
| 		{
 | |
| 			auto entity = all_entities.emplace_back(registry.create_entity());
 | |
| 			registry.add<Component>(entity, {});
 | |
| 		}
 | |
| 		expect_eq(on_construct_called, all_entities);
 | |
| 		expect_true(on_destruct_called.empty());
 | |
| 
 | |
| 		for (auto &entity : all_entities)
 | |
| 		{
 | |
| 			registry.remove<Component>(entity);
 | |
| 		}
 | |
| 		expect_eq(on_construct_called, all_entities);
 | |
| 		expect_eq(on_destruct_called, all_entities);
 | |
| 	};
 | |
| };
 | |
| 
 | |
| Suite each = "each"_suite = [] {
 | |
| 	auto registry = Registry {};
 | |
| 
 | |
| 	auto shared_entity_counter = 0u;
 | |
| 
 | |
| 	auto component_map_a = std::unordered_map<EntityId, Component> {};
 | |
| 	auto entities_a = std::vector<EntityId> {};
 | |
| 
 | |
| 	for (auto idx : std::views::iota(0, 10'000))
 | |
| 	{
 | |
| 		auto entity = entities_a.emplace_back(registry.create_entity());
 | |
| 		auto &component = registry.add<Component>(
 | |
| 		    entity,
 | |
| 		    { .m_int = idx, .m_string = std::to_string(idx) }
 | |
| 		);
 | |
| 
 | |
| 		component_map_a[entity] = component;
 | |
| 	}
 | |
| 
 | |
| 	auto component_map_b = std::unordered_map<lt::ecs::EntityId, Component_B> {};
 | |
| 	for (auto idx : std::views::iota(0, 10'000))
 | |
| 	{
 | |
| 		auto entity = EntityId {};
 | |
| 		if (idx % 3 == 0)
 | |
| 		{
 | |
| 			entity = entities_a[idx];
 | |
| 			++shared_entity_counter;
 | |
| 		}
 | |
| 		else
 | |
| 		{
 | |
| 			entity = registry.create_entity();
 | |
| 		}
 | |
| 		auto &component = registry.add<Component_B>(
 | |
| 		    entity,
 | |
| 		    { .m_float = static_cast<float>(idx) / 2.0f }
 | |
| 		);
 | |
| 
 | |
| 		component_map_b[entity] = component;
 | |
| 	}
 | |
| 
 | |
| 	Case { "each one element" } = [&] {
 | |
| 		auto counter = 0u;
 | |
| 		registry.each<Component>([&](EntityId entity, Component &component) {
 | |
| 			++counter;
 | |
| 			expect_eq(component_map_a[entity], component);
 | |
| 		});
 | |
| 
 | |
| 		expect_eq(component_map_a.size(), counter);
 | |
| 
 | |
| 		counter = 0u;
 | |
| 		registry.each<Component_B>([&](EntityId entity, Component_B &component) {
 | |
| 			++counter;
 | |
| 			expect_eq(component_map_b[entity], component);
 | |
| 		});
 | |
| 		expect_eq(component_map_b.size(), counter);
 | |
| 	};
 | |
| 
 | |
| 	Case { "each two element" } = [&] {
 | |
| 		auto counter = 0u;
 | |
| 		registry.each<Component, Component_B>(
 | |
| 		    [&](EntityId entity, Component &component_a, Component_B &component_b) {
 | |
| 			    expect_eq(component_map_a[entity], component_a);
 | |
| 			    expect_eq(component_map_b[entity], component_b);
 | |
| 			    ++counter;
 | |
| 		    }
 | |
| 		);
 | |
| 
 | |
| 		expect_eq(counter, shared_entity_counter);
 | |
| 	};
 | |
| };
 | |
| 
 | |
| Suite views = "views"_suite = [] {
 | |
| 	auto registry = Registry {};
 | |
| 
 | |
| 	auto shared_entity_counter = 0u;
 | |
| 
 | |
| 	auto component_map_a = std::unordered_map<EntityId, Component> {};
 | |
| 	auto entities_a = std::vector<EntityId> {};
 | |
| 
 | |
| 	for (auto idx : std::views::iota(0, 10'000))
 | |
| 	{
 | |
| 		auto entity = entities_a.emplace_back(registry.create_entity());
 | |
| 		auto &component = registry.add<Component>(
 | |
| 		    entity,
 | |
| 		    { .m_int = idx, .m_string = std::to_string(idx) }
 | |
| 		);
 | |
| 
 | |
| 		component_map_a[entity] = component;
 | |
| 	}
 | |
| 
 | |
| 	auto component_map_b = std::unordered_map<EntityId, Component_B> {};
 | |
| 	for (auto idx : std::views::iota(0, 10'000))
 | |
| 	{
 | |
| 		auto entity = EntityId {};
 | |
| 		if (idx % 3 == 0)
 | |
| 		{
 | |
| 			entity = entities_a[idx];
 | |
| 			++shared_entity_counter;
 | |
| 		}
 | |
| 		else
 | |
| 		{
 | |
| 			entity = registry.create_entity();
 | |
| 		}
 | |
| 		auto &component = registry.add<Component_B>(
 | |
| 		    entity,
 | |
| 		    { .m_float = static_cast<float>(idx) / 2.0f }
 | |
| 		);
 | |
| 
 | |
| 		component_map_b[entity] = component;
 | |
| 	}
 | |
| 
 | |
| 
 | |
| 	Case { "view one component" } = [&] {
 | |
| 		for (const auto &[entity, component] : registry.view<Component>())
 | |
| 		{
 | |
| 			expect_eq(component_map_a[entity], component);
 | |
| 		}
 | |
| 
 | |
| 		for (const auto &[entity, component] : registry.view<Component_B>())
 | |
| 		{
 | |
| 			expect_eq(component_map_b[entity], component);
 | |
| 		}
 | |
| 	};
 | |
| 
 | |
| 	Case { "view two component" } = [&] {
 | |
| 		auto counter = 0u;
 | |
| 		for (const auto &[entity, component, component_b] : registry.view<Component, Component_B>())
 | |
| 		{
 | |
| 			expect_eq(component_map_a[entity], component);
 | |
| 			expect_eq(component_map_b[entity], component_b);
 | |
| 			++counter;
 | |
| 		}
 | |
| 		expect_eq(counter, shared_entity_counter);
 | |
| 
 | |
| 		counter = 0u;
 | |
| 		for (const auto &[entity, component_b, component] : registry.view<Component_B, Component>())
 | |
| 		{
 | |
| 			expect_eq(component_map_b[entity], component_b);
 | |
| 			expect_eq(component_map_a[entity], component);
 | |
| 			++counter;
 | |
| 		}
 | |
| 		expect_eq(counter, shared_entity_counter);
 | |
| 	};
 | |
| };
 |