基于IQueryable与AST实现游戏内实体查询方案
我完全懂你说的那种“黑魔法”感觉——明明写的是和数据库无关的LINQ代码,转头就变成了优化后的SQL,就像代码自己偷偷开了外挂一样。先把这个机制拆得明明白白,再给你套到游戏的Entity和Chunk场景里,一步一步来。
先拆解那层“黑魔法”:从LINQ到SQL的核心流程
其实这玩意儿根本不是魔法,就是三个核心组件在协作:
- IQueryable
vs IEnumerable :普通的IEnumerable是拿到数据就直接执行遍历、筛选;但IQueryable是延迟执行的,它不直接跑代码,而是把你写的LINQ查询(比如Where、Select)转换成一棵抽象语法树(AST)——说白了就是把你的查询逻辑拆成结构化的“代码描述”,比如“筛选年龄大于18的用户”会被拆成“目标集合是Users,条件是Age属性大于常量18”。 - 查询提供者(Query Provider):这就是那个“翻译官”。当你调用
ToList()、First()这类触发执行的方法时,IQueryable会把AST交给它,它会把这棵树翻译成目标系统能懂的语言——比如SQL,或者游戏里的高效查询逻辑。 - 举个接地气的例子:你写
dbContext.Users.Where(u => u.Age > 18),得到的不是筛选后的用户列表,而是一个IQueryable<User>,里面装着描述这个筛选逻辑的AST。等你调用ToList(),查询提供者就把AST翻译成SELECT * FROM Users WHERE Age > 18,发给数据库执行,再把结果转成C#对象给你。
把这套逻辑移植到游戏的Entity/Chunk场景
你的场景里有Entity(带世界坐标position)和Chunk(固定16单位大小,带块坐标position),核心需求应该是高效查询某个Chunk里的所有Entity,或者反过来查某个Entity属于哪个Chunk?用AST的思路,你可以自己实现一个类似的查询系统,不用写一堆硬编码的筛选逻辑,而是用LINQ风格的代码,让“翻译官”帮你做最优查询。
第一步:搭建游戏专属的查询框架
首先,你需要一套类似IQueryable的接口,用来持有查询的AST,再写一个“游戏查询提供者”来解析这个AST,转换成游戏里的高效操作(比如直接定位到目标Chunk,而不是遍历所有Entity)。
第二步:实现AST解析逻辑
比如,当你写Entities.Where(e => e.position.X >= chunkPos.X*16 && e.position.X < (chunkPos.X+1)*16 && ...),你的查询提供者需要解析这个Lambda表达式的AST,识别出你是在根据Chunk的边界筛选Entity,然后直接调用Chunk管理器找到对应的Chunk,取出里面的Entity——这比遍历所有Entity快多了,尤其是当Entity数量上万的时候。
具体代码示例
先把基础的Entity、Chunk和管理器写好:
public class Vector2 { public float X { get; set; } public float Y { get; set; } public Vector2(float x, float y) { X = x; Y = y; } } public class Entity { public Vector2 Position { get; set; } // 其他Entity属性,比如ID、类型之类的 } public class Chunk { public const int CHUNK_SIZE = 16; public Vector2 ChunkPosition { get; set; } // 块的坐标,比如(0,0)对应世界坐标0-16的区域 public List<Entity> EntitiesInChunk { get; set; } = new(); } // 全局Chunk管理器,负责维护所有Chunk的映射 public static class ChunkManager { public static Dictionary<(int X, int Y), Chunk> Chunks { get; } = new(); // 把世界坐标转换成Chunk坐标的工具方法 public static (int X, int Y) WorldPosToChunkPos(Vector2 worldPos) { return ((int)Math.Floor(worldPos.X / Chunk.CHUNK_SIZE), (int)Math.Floor(worldPos.Y / Chunk.CHUNK_SIZE)); } }
然后实现游戏的查询接口和提供者:
// 类似IQueryable的接口,持有查询的AST和对应的提供者 public interface IGameQueryable<T> : IEnumerable<T> { Expression Expression { get; } IGameQueryProvider Provider { get; } } public interface IGameQueryProvider { IGameQueryable<T> CreateQuery<T>(Expression expression); TResult Execute<TResult>(Expression expression); } // 专门处理Entity查询的提供者 public class EntityQueryProvider : IGameQueryProvider { public IGameQueryable<T> CreateQuery<T>(Expression expression) { return new GameQuery<T>(this, expression); } public TResult Execute<TResult>(Expression expression) { // 这里我们重点处理Where查询,解析AST里的筛选条件 if (expression is MethodCallExpression methodCall && methodCall.Method.Name == "Where") { // 取出Lambda表达式(用户写的筛选逻辑) var lambda = ((UnaryExpression)methodCall.Arguments[1]).Operand as LambdaExpression; // 这里可以用ExpressionVisitor来遍历AST,提取出用户要筛选的position范围 // 为了简化,我们直接模拟解析出目标Chunk的坐标(实际项目中可以写更通用的解析逻辑) // 比如从Lambda的Body里提取出X和Y的范围,计算出对应的Chunk坐标 var targetChunkPos = ChunkManager.WorldPosToChunkPos(new Vector2(0, 0)); // 这里替换成从AST提取的坐标 // 直接从Chunk管理器拿对应的Chunk里的Entity,不用遍历所有Entity if (ChunkManager.Chunks.TryGetValue(targetChunkPos, out var chunk)) { return (TResult)(object)chunk.EntitiesInChunk; } return (TResult)(object)new List<Entity>(); } // 其他查询方法(比如Select、OrderBy)可以在这里扩展 throw new NotSupportedException($"查询方法 {expression.NodeType} 暂不支持"); } } // 具体的查询实现类 public class GameQuery<T> : IGameQueryable<T> { public Expression Expression { get; } public IGameQueryProvider Provider { get; } public GameQuery(IGameQueryProvider provider, Expression expression) { Provider = provider; Expression = expression; } public IEnumerator<T> GetEnumerator() { return Provider.Execute<IEnumerable<T>>(Expression).GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } // 扩展方法,让我们可以用LINQ风格的语法写查询 public static class GameQueryExtensions { public static IGameQueryable<Entity> Where(this IGameQueryable<Entity> source, Expression<Func<Entity, bool>> predicate) { return source.Provider.CreateQuery<Entity>( Expression.Call( typeof(Enumerable), "Where", new[] { typeof(Entity) }, source.Expression, Expression.Quote(predicate) ) ); } }
怎么用这套框架
// 创建一个可查询的Entity集合 var queryableEntities = new GameQuery<Entity>(new EntityQueryProvider(), Expression.Constant(new List<Entity>())); // 写LINQ风格的查询,完全不用关心底层怎么找Chunk var targetWorldPos = new Vector2(8, 8); // 随便选一个世界坐标 var entitiesInChunk = queryableEntities.Where(e => e.Position.X >= targetWorldPos.X - targetWorldPos.X % Chunk.CHUNK_SIZE && e.Position.X < targetWorldPos.X - targetWorldPos.X % Chunk.CHUNK_SIZE + Chunk.CHUNK_SIZE && e.Position.Y >= targetWorldPos.Y - targetWorldPos.Y % Chunk.CHUNK_SIZE && e.Position.Y < targetWorldPos.Y - targetWorldPos.Y % Chunk.CHUNK_SIZE + Chunk.CHUNK_SIZE ).ToList();
这么做的好处
- 逻辑解耦:上层代码只需要写“我要找符合XX条件的Entity”,不用关心底层是用Chunk、四叉树还是其他空间索引来实现的——以后要换索引方式,只改查询提供者就行。
- 类型安全:用LINQ的语法,编译时就能检查错误,比写字符串查询或者硬编码筛选靠谱多了。
- 可扩展性:以后要加
OrderBy、Select这类查询,只需要在查询提供者里加对应的解析逻辑就行。
内容的提问来源于stack exchange,提问作者markonius

