You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于IQueryable与AST实现游戏内实体查询方案

理解IQueryable/Expression的AST转换机制,并应用到游戏Entity/Chunk场景

我完全懂你说的那种“黑魔法”感觉——明明写的是和数据库无关的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.26 09:51:35