updated observer (close #260)

This commit is contained in:
Michele Caini
2019-06-11 17:49:08 +02:00
parent 43932492a7
commit 16a11638a7
3 changed files with 279 additions and 70 deletions

View File

@@ -432,10 +432,10 @@ from the documentation of the library that first introduced this tool,
>only update the 10 changed units. So efficient.
In `EnTT`, this means to iterating over a reduced set of entities and components
with respect to what would otherwise be returned from a view or group.<br/>
On these words, however, the similarities with the proposal of Entitas also end.
The rules of language and the design of the library obviously impose and allow
different things.
with respect to what would otherwise be returned from a view or a group.<br/>
On these words, however, the similarities with the proposal of `Entitas` also
end. The rules of language and the design of the library obviously impose and
allow different things.
An `observer` is initialized with an instance of a registry and a set of rules
that describe what are the entities to intercept. As an example:
@@ -444,23 +444,24 @@ that describe what are the entities to intercept. As an example:
entt::observer observer{registry, entt::collector.replace<sprite>()};
```
The class also default constructible if required and it can be reconfigured at
any time by means of the `connect` member function. Moreover, instances can be
The class is default constructible if required and it can be reconfigured at any
time by means of the `connect` member function. Moreover, instances can be
disconnected from the underlying registries through the `disconnect` member
function.<br/>
The `observer` offers also some member functions to query its internal state and
to know if it's empty or how many entities it contains. Moreover, it can return
a raw pointer to the list of entities it contains.<br/>
a raw pointer to the list of entities it contains.
However, the most important features of this class are that:
* It's iterable and therefore users can easily walk through the list of entities
by means of a range-for loop.
by means of a range-for loop or the `each` member function.
* It's clearable and therefore users can consume the entities and literally
reset the observer after each iteration.
These aspects make the observer an incredibly powerful tool to know at any time
what are the entities that have started to respect the given rules since the
last time one asked:
what are the entities that matched the given rules since the last time one
asked:
```cpp
for(const auto entity: observer) {
@@ -470,8 +471,20 @@ for(const auto entity: observer) {
observer.clear();
```
The `collector` is an utility to use to generate a list of `matcher`s (the
actual rules) to use with an `observer`.<br/>
Note that the snippet above is equivalent to the following:
```cpp
observer.each([](const auto entity) {
// ...
});
```
At least as long as the `observer` isn't const. This means that the non-const
overload of `each` does also reset the underlying data structure before to
return to the caller, while the const overload does not for obvious reasons.
The `collector` is an utility aimed to generate a list of `matcher`s (the actual
rules) to use with an `observer` instead.<br/>
There are two types of `matcher`s:
* Observing matcher: an observer will return at least all the living entities
@@ -479,7 +492,7 @@ There are two types of `matcher`s:
and not yet destroyed.
```cpp
collector.replace<sprite>();
entt::collector.replace<sprite>();
```
* Grouping matcher: an observer will return at least all the living entities
@@ -487,7 +500,7 @@ There are two types of `matcher`s:
not yet left it.
```cpp
collector.group<position, velocity>(entt::exclude<destroyed>);
entt::collector.group<position, velocity>(entt::exclude<destroyed>);
```
A grouping matcher supports also exclusion lists as well as single components.
@@ -499,6 +512,25 @@ last time one asked.<br/>
Note that, for a grouping matcher, if an entity already has all the components
except one and the missing type is assigned to it, it is intercepted.
In addition, a matcher can be filtered with a `when` clause:
```cpp
entt::collector.replace<sprite>().when<position>(entt::exclude<velocity>);
```
This clause introduces a way to intercept entities if and only if they are
already part of a hypothetical group. If they are not, they aren't returned by
the observer, no matter if they matched the given rule.<br/>
In the example above, whenever the component `sprite` of an entity is replaced,
the observer probes the entity itself to verify that it has at least `position`
and has not `velocity` before to store it aside. If one of the two conditions of
the filter isn't respected, the entity is discared, no matter what.
A `when` clause accepts a theoretically unlimited number of types as well as
multiple elements in the exclusion list. Moreover, every matcher can have it's
own clause and multiple clauses for the same matcher are combined in a single
one.
## Runtime components
Defining components at runtime is useful to support plugin systems and mods in

View File

@@ -6,6 +6,8 @@
#include <cstddef>
#include <cstdint>
#include <utility>
#include <algorithm>
#include <type_traits>
#include "../config/config.h"
#include "../core/type_traits.hpp"
#include "registry.hpp"
@@ -17,46 +19,19 @@
namespace entt {
/*! @brief Grouping matcher. */
template<typename...>
struct matcher {};
/**
* @brief Matcher.
* @brief Collector.
*
* Primary template isn't defined on purpose. All the specializations give a
* compile-time error, but for a few reasonable cases.
*/
template<typename...>
struct matcher;
/**
* @brief Observing matcher.
*
* An observing matcher contains a type for which changes should be
* detected.<br/>
* Because of the rules of the language, not all changes can be easily detected.
* In order to avoid nasty solutions that could affect performance to an extent,
* the matcher listens only to the `on_replace` signals emitted by a registry
* and is therefore triggered whenever an instance of the given component is
* explicitly replaced.
*
* @tparam AnyOf Type of component for which changes should be detected.
*/
template<typename AnyOf>
struct matcher<AnyOf> {};
/**
* @brief Grouping matcher.
*
* A grouping matcher describes the group to track in terms of accepted and
* excluded types.<br/>
* This kind of matcher is triggered whenever an entity _enters_ the desired
* group because of the components it is assigned.
*
* @tparam AllOf Types of components tracked by the matcher.
* @tparam NoneOf Types of components used to filter out entities.
*/
template<typename... AllOf, typename... NoneOf>
struct matcher<type_list<AllOf...>, type_list<NoneOf...>> {};
struct basic_collector;
/**
@@ -66,12 +41,9 @@ struct matcher<type_list<AllOf...>, type_list<NoneOf...>> {};
* entities.<br/>
* Its main purpose is to generate a descriptor that allows an observer to know
* how to connect to a registry.
*
* @tparam AnyOf Types of components for which changes should be detected.
* @tparam Matcher Types of grouping matchers.
*/
template<typename... Matcher>
struct basic_collector {
template<>
struct basic_collector<> {
/**
* @brief Adds a grouping matcher to the collector.
* @tparam AllOf Types of components tracked by the matcher.
@@ -80,17 +52,60 @@ struct basic_collector {
*/
template<typename... AllOf, typename... NoneOf>
static constexpr auto group(exclude_t<NoneOf...> = {}) ENTT_NOEXCEPT {
return basic_collector<Matcher..., matcher<type_list<AllOf...>, type_list<NoneOf...>>>{};
return basic_collector<matcher<matcher<type_list<>, type_list<>>, type_list<NoneOf...>, type_list<AllOf...>>>{};
}
/**
* @brief Adds one or more observing matchers to the collector.
* @tparam AnyOf Types of components for which changes should be detected.
* @brief Adds an observing matcher to the collector.
* @tparam AnyOf Type of component for which changes should be detected.
* @return The updated collector.
*/
template<typename... AnyOf>
template<typename AnyOf>
static constexpr auto replace() ENTT_NOEXCEPT {
return basic_collector<Matcher..., matcher<AnyOf>...>{};
return basic_collector<matcher<matcher<type_list<>, type_list<>>, AnyOf>>{};
}
};
/**
* @brief Collector.
* @copydetails basic_collector<>
* @tparam AnyOf Types of components for which changes should be detected.
* @tparam Matcher Types of grouping matchers.
*/
template<typename... Reject, typename... Require, typename... Rule, typename... Other>
struct basic_collector<matcher<matcher<type_list<Reject...>, type_list<Require...>>, Rule...>, Other...> {
/**
* @brief Adds a grouping matcher to the collector.
* @tparam AllOf Types of components tracked by the matcher.
* @tparam NoneOf Types of components used to filter out entities.
* @return The updated collector.
*/
template<typename... AllOf, typename... NoneOf>
static constexpr auto group(exclude_t<NoneOf...> = {}) ENTT_NOEXCEPT {
using first = matcher<matcher<type_list<Reject...>, type_list<Require...>>, Rule...>;
return basic_collector<first, Other..., matcher<matcher<type_list<>, type_list<>>, type_list<NoneOf...>, type_list<AllOf...>>>{};
}
/**
* @brief Adds an observing matcher to the collector.
* @tparam AnyOf Type of component for which changes should be detected.
* @return The updated collector.
*/
template<typename AnyOf>
static constexpr auto replace() ENTT_NOEXCEPT {
using first = matcher<matcher<type_list<Reject...>, type_list<Require...>>, Rule...>;
return basic_collector<first, Other..., matcher<matcher<type_list<>, type_list<>>, AnyOf>>{};
}
/**
* @brief Updates the filter of the last added matcher.
* @tparam AllOf Types of components required by the matcher.
* @tparam NoneOf Types of components used to filter out entities.
* @return The updated collector.
*/
template<typename... AllOf, typename... NoneOf>
static constexpr auto when(exclude_t<NoneOf...> = {}) ENTT_NOEXCEPT {
return basic_collector<matcher<matcher<type_list<Reject..., NoneOf...>, type_list<Require..., AllOf...>>, Rule...>, Other...>{};
}
};
@@ -120,6 +135,13 @@ constexpr basic_collector<> collector{};
* If an entity respects the requirements of multiple matchers, it will be
* returned once and only once by the observer in any case.
*
* Matchers support also filtering by means of a _when_ clause that accepts both
* a list of types and an exclusion list.<br/>
* Whenever a matcher finds that an entity matches its requirements, the
* condition of the filter is verified before to register the entity itself.
* Moreover, a registered entity isn't returned by the observer if the condition
* set by the filter is broken in the meantime.
*
* @b Important
*
* Iterators aren't invalidated if:
@@ -149,10 +171,12 @@ class basic_observer {
template<std::size_t Index, typename>
struct matcher_handler;
template<std::size_t Index, typename AnyOf>
struct matcher_handler<Index, matcher<AnyOf>> {
static void maybe_valid_if(basic_observer *obs, const basic_registry<Entity> &, const Entity entt) {
(obs->view.has(entt) ? obs->view.get(entt) : obs->view.construct(entt)) |= (1 << Index);
template<std::size_t Index, typename... Reject, typename... Require, typename AnyOf>
struct matcher_handler<Index, matcher<matcher<type_list<Reject...>, type_list<Require...>>, AnyOf>> {
static void maybe_valid_if(basic_observer *obs, const basic_registry<Entity> &reg, const Entity entt) {
if(reg.template has<Require...>(entt) && !(reg.template has<Reject>(entt) || ...)) {
(obs->view.has(entt) ? obs->view.get(entt) : obs->view.construct(entt)) |= (1 << Index);
}
}
static void discard_if(basic_observer *obs, const basic_registry<Entity> &, const Entity entt) {
@@ -162,20 +186,26 @@ class basic_observer {
}
static void disconnect(basic_registry<Entity> &reg, const basic_observer &obs) {
(reg.template on_replace<AnyOf>().disconnect(&obs));
(reg.template on_destroy<AnyOf>().disconnect(&obs));
reg.template on_replace<AnyOf>().disconnect(&obs);
reg.template on_destroy<AnyOf>().disconnect(&obs);
(reg.template on_destroy<Require>().disconnect(&obs), ...);
(reg.template on_construct<Reject>().disconnect(&obs), ...);
}
static void connect(basic_observer &obs, basic_registry<Entity> &reg) {
reg.template on_replace<AnyOf>().template connect<&maybe_valid_if>(&obs);
reg.template on_destroy<AnyOf>().template connect<&discard_if>(&obs);
(reg.template on_destroy<Require>().template connect<&discard_if>(&obs), ...);
(reg.template on_construct<Reject>().template connect<&discard_if>(&obs), ...);
}
};
template<std::size_t Index, typename... AllOf, typename... NoneOf>
struct matcher_handler<Index, matcher<type_list<AllOf...>, type_list<NoneOf...>>> {
template<std::size_t Index, typename... Reject, typename... Require, typename... NoneOf, typename... AllOf>
struct matcher_handler<Index, matcher<matcher<type_list<Reject...>, type_list<Require...>>, type_list<NoneOf...>, type_list<AllOf...>>> {
static void maybe_valid_if(basic_observer *obs, const basic_registry<Entity> &reg, const Entity entt) {
if(reg.template has<AllOf...>(entt) && !(reg.template has<NoneOf>(entt) || ...)) {
if(reg.template has<AllOf...>(entt) && !(reg.template has<NoneOf>(entt) || ...)
&& reg.template has<Require...>(entt) && !(reg.template has<Reject>(entt) || ...))
{
(obs->view.has(entt) ? obs->view.get(entt) : obs->view.construct(entt)) |= (1 << Index);
}
}
@@ -191,6 +221,8 @@ class basic_observer {
((reg.template on_destroy<AllOf>().disconnect(&obs)), ...);
((reg.template on_construct<NoneOf>().disconnect(&obs)), ...);
((reg.template on_destroy<NoneOf>().disconnect(&obs)), ...);
(reg.template on_destroy<Require>().disconnect(&obs), ...);
(reg.template on_construct<Reject>().disconnect(&obs), ...);
}
static void connect(basic_observer &obs, basic_registry<Entity> &reg) {
@@ -198,6 +230,8 @@ class basic_observer {
(reg.template on_destroy<NoneOf>().template connect<&maybe_valid_if>(&obs), ...);
(reg.template on_destroy<AllOf>().template connect<&discard_if>(&obs), ...);
(reg.template on_construct<NoneOf>().template connect<&discard_if>(&obs), ...);
(reg.template on_destroy<Require>().template connect<&discard_if>(&obs), ...);
(reg.template on_construct<Reject>().template connect<&discard_if>(&obs), ...);
}
};
@@ -341,7 +375,47 @@ public:
/*! @brief Resets the underlying container. */
void clear() {
return view.reset();
view.reset();
}
/**
* @brief Iterates entities and applies the given function object to them,
* then clears the observer.
*
* The function object is invoked for each entity.<br/>
* The signature of the function must be equivalent to the following form:
*
* @code{.cpp}
* void(const entity_type);
* @endcode
*
* @tparam Func Type of the function object to invoke.
* @param func A valid function object.
*/
template<typename Func>
void each(Func func) const {
static_assert(std::is_invocable_v<Func, entity_type>);
std::for_each(begin(), end(), std::move(func));
}
/**
* @brief Iterates entities and applies the given function object to them,
* then clears the observer.
*
* The function object is invoked for each entity.<br/>
* The signature of the function must be equivalent to the following form:
*
* @code{.cpp}
* void(const entity_type);
* @endcode
*
* @tparam Func Type of the function object to invoke.
* @param func A valid function object.
*/
template<typename Func>
void each(Func func) {
std::as_const(*this).each(std::move(func));
clear();
}
private:

View File

@@ -1,3 +1,5 @@
#include <tuple>
#include <type_traits>
#include <gtest/gtest.h>
#include <entt/entity/observer.hpp>
#include <entt/entity/registry.hpp>
@@ -82,9 +84,49 @@ TEST(Observer, AllOf) {
ASSERT_TRUE(observer.empty());
}
TEST(Observer, AllOfFiltered) {
constexpr auto collector = entt::collector
.group<int>().when<char>(entt::exclude<double>);
entt::registry registry;
entt::observer observer{registry, collector};
const auto entity = registry.create();
ASSERT_TRUE(observer.empty());
registry.assign<int>(entity);
ASSERT_EQ(observer.size(), entt::observer::size_type{});
ASSERT_TRUE(observer.empty());
ASSERT_EQ(observer.data(), nullptr);
registry.remove<int>(entity);
registry.assign<char>(entity);
registry.assign<double>(entity);
registry.assign<int>(entity);
ASSERT_TRUE(observer.empty());
registry.remove<int>(entity);
registry.remove<double>(entity);
registry.assign<int>(entity);
ASSERT_EQ(observer.size(), entt::observer::size_type{1});
ASSERT_FALSE(observer.empty());
ASSERT_EQ(*observer.data(), entity);
registry.assign<double>(entity);
ASSERT_TRUE(observer.empty());
registry.remove<double>(entity);
ASSERT_TRUE(observer.empty());
}
TEST(Observer, Observe) {
entt::registry registry;
entt::observer observer{registry, entt::collector.replace<int, char>()};
entt::observer observer{registry, entt::collector.replace<int>().replace<char>()};
const auto entity = registry.create();
ASSERT_TRUE(observer.empty());
@@ -116,6 +158,45 @@ TEST(Observer, Observe) {
ASSERT_TRUE(observer.empty());
}
TEST(Observer, ObserveFiltered) {
constexpr auto collector = entt::collector
.replace<int>().when<char>(entt::exclude<double>);
entt::registry registry;
entt::observer observer{registry, collector};
const auto entity = registry.create();
ASSERT_TRUE(observer.empty());
registry.assign<int>(entity);
registry.replace<int>(entity);
ASSERT_EQ(observer.size(), entt::observer::size_type{});
ASSERT_TRUE(observer.empty());
ASSERT_EQ(observer.data(), nullptr);
registry.assign<char>(entity);
registry.assign<double>(entity);
registry.replace<int>(entity);
ASSERT_TRUE(observer.empty());
registry.remove<double>(entity);
registry.replace<int>(entity);
ASSERT_EQ(observer.size(), entt::observer::size_type{1});
ASSERT_FALSE(observer.empty());
ASSERT_EQ(*observer.data(), entity);
registry.assign<double>(entity);
ASSERT_TRUE(observer.empty());
registry.remove<double>(entity);
ASSERT_TRUE(observer.empty());
}
TEST(Observer, AllOfObserve) {
entt::registry registry;
entt::observer observer{};
@@ -151,7 +232,6 @@ TEST(Observer, AllOfObserve) {
ASSERT_TRUE(observer.empty());
}
TEST(Observer, CrossRulesCornerCase) {
entt::registry registry;
entt::observer observer{registry, entt::collector.group<int>().group<char>()};
@@ -167,3 +247,26 @@ TEST(Observer, CrossRulesCornerCase) {
ASSERT_FALSE(observer.empty());
}
TEST(Observer, Each) {
entt::registry registry;
entt::observer observer{registry, entt::collector.group<int>()};
const auto entity = std::get<0>(registry.create<int>());
ASSERT_FALSE(observer.empty());
ASSERT_EQ(observer.size(), entt::observer::size_type{1});
std::as_const(observer).each([entity](const auto entt) {
ASSERT_EQ(entity, entt);
});
ASSERT_FALSE(observer.empty());
ASSERT_EQ(observer.size(), entt::observer::size_type{1});
observer.each([entity](const auto entt) {
ASSERT_EQ(entity, entt);
});
ASSERT_TRUE(observer.empty());
ASSERT_EQ(observer.size(), entt::observer::size_type{});
}