Skip to content

Commit d2cd2a9

Browse files
author
Andrey Cherkashin
committed
feat: update for Morpeh 2024.1.1 compatibility
Add new system base classes (QueryFixedSystem, QueryLateSystem, QueryCleanupSystem), ForFirst query overloads, Extend<T> support, and CompiledQuery.Count(). Fix FilterBuilder struct compatibility (Also callback), reflection invocation (Build), empty filter crash, validation bug, compile errors without MORPEH_BURST, and rename typo in EventListenerExtensions. Cache MakeGenericMethod results in QueryBuilder.Build() for better performance.
1 parent 4e12b5a commit d2cd2a9

16 files changed

+508
-43
lines changed

CHANGELOG.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Changelog
2+
3+
All notable changes to Morpeh.Queries will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
6+
7+
---
8+
9+
## [2024.1.1] — 2026-02-23
10+
11+
### Added
12+
13+
- **`QueryFixedSystem`** — base class combining `QuerySystem` + `IFixedSystem` for FixedUpdate systems.
14+
- **`QueryLateSystem`** — base class combining `QuerySystem` + `ILateSystem` for LateUpdate systems.
15+
- **`QueryCleanupSystem`** — base class combining `QuerySystem` + `ICleanupSystem` for cleanup systems.
16+
- **`CompiledQuery.Count()`** — returns the number of matching entities via `filter.GetLengthSlow()`.
17+
- **`QueryBuilder.Extend<T>()`** — support for `IFilterExtension` custom filter extensions.
18+
- **`ForFirst` overloads** in `QueryBuilderExtensions` — execute a lambda for the *first* matching entity only. Available in all variants:
19+
- `ForFirst(Action<Entity>)` — entity only
20+
- `ForFirst<T1>(Action<Entity, T1>)` / `ForFirst<T1>(Action<T1>)` — 1 component
21+
- `ForFirst<T1,T2>(...)` — 2 components
22+
- `ForFirst<T1,T2,T3>(...)` — 3 components
23+
- `ForFirst<T1,T2,T3,T4>(...)` — 4 components
24+
25+
### Fixed
26+
27+
- **Morpeh 2024.1.1 compatibility — `QueryBuilder.Build()` reflection**: `FilterBuilder.With<T>()` and `Without<T>()` are now zero-parameter instance methods. Fixed invocation to pass `null` instead of `new object[] { filterBuilder }`, preventing `TargetParameterCountException` at runtime.
28+
- **Morpeh 2024.1.1 compatibility — `CompiledQuery` empty filter crash**: In Morpeh 2024.1.1 an empty `Filter` matches *all* archetypes, making direct `world.entities[]` iteration unnecessary and unsafe (null archetype slots). Fixed by always using the `Filter` enumerator regardless of whether include/exclude types are present.
29+
- **Morpeh 2024.1.1 compatibility — `QueryBuilderExtensions.Also()`**: `FilterBuilder` changed from a class to a `struct` in Morpeh 2024.1.1. The previous `Action<FilterBuilder>` signature silently discarded all modifications. Fixed by changing to `Func<FilterBuilder, FilterBuilder>`.
30+
- **`QueryHelper.ValidateRequest`**: `hasProblems` was declared but never set to `true` inside error branches, meaning validation exceptions were never thrown even when components were missing from the filter. Fixed.
31+
- **`QuerySystem` compile error without `MORPEH_BURST`**: `IsUpdatedEveryFrame`, `AddExecutor()`, `CreateQuery()`, and `m_executors` were incorrectly placed inside `#if MORPEH_BURST`, causing compile errors in non-Burst projects. Moved outside the conditional block.
32+
- **`EventListenerExtensions` method name**: `CompiledCompiledEventListener` (duplicate word) renamed to `ForEach`.
33+
34+
### Improved
35+
36+
- **`QueryBuilder.Build()` performance**: `MakeGenericMethod` results are now cached in static `Dictionary<Type, MethodInfo>` dictionaries (`s_withMethodCache`, `s_withoutMethodCache`), eliminating repeated reflection allocations on every `Build()` call.
37+
38+
---
39+
40+
## [2024.1] — initial release
41+
42+
- Initial release of Morpeh.Queries.
43+
- Lambda-based `ForEach` and `ForEachParallel` queries.
44+
- Jobs & Burst support via `ScheduleJob`.
45+
- World Events and Entity Events system.
46+
- `QueryBuilderGlobals` for project-wide filter constraints.
47+
- Automatic query validation (`QueryHelper`).

CHANGELOG.md.meta

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Core/CompiledQuery.cs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,25 +66,27 @@ public void Dispose()
6666
public CompiledQuery(Filter filter)
6767
{
6868
this.filter = filter;
69-
hasFilter = filter.excludedTypeIds.Length > 0 || filter.includedTypeIds.Length > 0;
69+
// In Morpeh 2024.1.1+, an empty filter (no includes/excludes) matches ALL archetypes,
70+
// so we always use filter-based iteration instead of the raw EntityData[] path.
71+
hasFilter = true;
7072
}
7173

7274
public bool IsEmpty()
7375
{
74-
if (!hasFilter)
75-
return filter.world.entities.Length == 0;
76-
7776
return filter.IsEmpty();
7877
}
7978

8079
public WorldEntitiesEnumerator GetEnumerator()
8180
{
82-
if (!hasFilter)
83-
return new WorldEntitiesEnumerator(filter.world.entities);
84-
8581
return new WorldEntitiesEnumerator(filter.GetEnumerator());
8682
}
8783

84+
/// <summary>
85+
/// Returns the total number of entities matched by this query.
86+
/// Note: this iterates all archetypes, prefer IsEmpty() if you only need an emptiness check.
87+
/// </summary>
88+
public int Count() => filter.GetLengthSlow();
89+
8890
public NativeFilter AsNative()
8991
{
9092
return filter.AsNative();

Core/QueryBuilder.cs

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
using System.Reflection;
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Reflection;
24

35
namespace Scellecs.Morpeh
46
{
@@ -7,6 +9,10 @@ public class QueryBuilder
79
private static readonly MethodInfo FILTER_WITH_METHOD_INFO = typeof(FilterBuilder).GetMethod("With");
810
private static readonly MethodInfo FILTER_WITHOUT_METHOD_INFO = typeof(FilterBuilder).GetMethod("Without");
911

12+
// Cache closed-generic MethodInfos to avoid MakeGenericMethod allocations on every Build() call
13+
private static readonly Dictionary<Type, MethodInfo> s_withMethodCache = new();
14+
private static readonly Dictionary<Type, MethodInfo> s_withoutMethodCache = new();
15+
1016
private readonly IQuerySystem m_querySystem;
1117
internal FilterBuilder filterBuilder;
1218

@@ -28,19 +34,39 @@ public CompiledQuery Build()
2834
if (QueryBuilderGlobals.TYPES_TO_REQUIRE.Count > 0)
2935
{
3036
for (var i = 0; i < QueryBuilderGlobals.TYPES_TO_REQUIRE.Count; i++)
31-
filterBuilder = (FilterBuilder)FILTER_WITH_METHOD_INFO.MakeGenericMethod(QueryBuilderGlobals.TYPES_TO_REQUIRE[i]).Invoke(filterBuilder, new object[] { filterBuilder });
37+
{
38+
var type = QueryBuilderGlobals.TYPES_TO_REQUIRE[i];
39+
if (!s_withMethodCache.TryGetValue(type, out var method))
40+
s_withMethodCache[type] = method = FILTER_WITH_METHOD_INFO.MakeGenericMethod(type);
41+
filterBuilder = (FilterBuilder)method.Invoke(filterBuilder, null);
42+
}
3243
}
3344

3445
if (QueryBuilderGlobals.TYPES_TO_IGNORE.Count > 0)
3546
{
3647
for (var i = 0; i < QueryBuilderGlobals.TYPES_TO_IGNORE.Count; i++)
37-
filterBuilder = (FilterBuilder)FILTER_WITHOUT_METHOD_INFO.MakeGenericMethod(QueryBuilderGlobals.TYPES_TO_IGNORE[i]).Invoke(filterBuilder, new object[] { filterBuilder });
48+
{
49+
var type = QueryBuilderGlobals.TYPES_TO_IGNORE[i];
50+
if (!s_withoutMethodCache.TryGetValue(type, out var method))
51+
s_withoutMethodCache[type] = method = FILTER_WITHOUT_METHOD_INFO.MakeGenericMethod(type);
52+
filterBuilder = (FilterBuilder)method.Invoke(filterBuilder, null);
53+
}
3854
}
3955
}
4056

4157
return new CompiledQuery(filterBuilder.Build());
4258
}
4359

60+
/// <summary>
61+
/// Applies a Morpeh IFilterExtension to this query.
62+
/// Allows reusing pre-defined filter compositions across multiple queries.
63+
/// </summary>
64+
public QueryBuilder Extend<T>() where T : struct, IFilterExtension
65+
{
66+
filterBuilder = filterBuilder.Extend<T>();
67+
return this;
68+
}
69+
4470
#region Parameters
4571

4672
public QueryBuilder SkipValidation(bool skipValidation)

Core/QueryCleanupSystem.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace Scellecs.Morpeh
2+
{
3+
/// <summary>
4+
/// Base class for cleanup systems that run at the end of the frame (LateUpdate order).
5+
/// </summary>
6+
public abstract class QueryCleanupSystem : QuerySystem, ICleanupSystem
7+
{
8+
}
9+
}

Core/QueryCleanupSystem.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Core/QueryFixedSystem.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
namespace Scellecs.Morpeh
2+
{
3+
/// <summary>
4+
/// Base class for systems that run in FixedUpdate.
5+
/// Inherit from this instead of QuerySystem when registering via group.AddSystem
6+
/// and you need physics-step timing.
7+
/// </summary>
8+
public abstract class QueryFixedSystem : QuerySystem, IFixedSystem
9+
{
10+
}
11+
}

Core/QueryFixedSystem.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Core/QueryHelper.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,18 @@ internal static void ValidateRequest(QueryBuilder queryBuilder, CompiledQuery co
3939
foreach (var requestedTypeInfo in requestedTypeInfosToValidate)
4040
{
4141
if (!compiledQuery.filter.includedTypeIdsLookup.IsSet(requestedTypeInfo.typeId))
42+
{
4243
Debug.LogError(
4344
$"You're expecting a component [<b>{requestedTypeInfo.type.Name}</b>] in your query in [<b>{queryBuilder.System.GetType().Name}</b>], but the query is <b>missing</b> this parameter. Please add it to the query first!");
45+
hasProblems = true;
46+
}
4447

4548
if (compiledQuery.filter.excludedTypeIdsLookup.IsSet(requestedTypeInfo.typeId))
49+
{
4650
Debug.LogError(
4751
$"You're expecting a component [<b>{requestedTypeInfo.type.Name}</b>] in your query in [<b>{queryBuilder.System.GetType().Name}</b>], but the query is <b>deliberately excluded</b> this parameter. Please remove it from the query or from the ForEach lambda!");
52+
hasProblems = true;
53+
}
4854
}
4955

5056
if (hasProblems)

Core/QueryLateSystem.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace Scellecs.Morpeh
2+
{
3+
/// <summary>
4+
/// Base class for systems that run in LateUpdate.
5+
/// </summary>
6+
public abstract class QueryLateSystem : QuerySystem, ILateSystem
7+
{
8+
}
9+
}

0 commit comments

Comments
 (0)