代码之家  ›  专栏  ›  技术社区  ›  akarnokd

面对EDT如何管理游戏状态?

  •  8
  • akarnokd  · 技术社区  · 15 年前

    我在Java平台上开发了一个实时策略游戏克隆,我有一些概念问题,关于放在哪里以及如何管理游戏状态。游戏使用Swing/Java2d作为渲染。在当前的开发阶段,没有模拟和人工智能,只有用户能够改变游戏的状态(例如,建造/拆除一栋大楼,添加/移除生产线,组装车队和设备)。因此,可以在事件调度线程中执行游戏状态操作,而无需任何呈现查找。游戏状态还用于向用户显示各种聚合信息。

    但是,由于我需要引入模拟(例如,构建进度、人口变化、车队移动、制造过程等),在计时器和EDT中更改游戏状态肯定会减慢渲染速度。

    假设模拟/ai操作每500毫秒执行一次,我使用swingworker计算大约250毫秒的长度。我如何确保在模拟和可能的用户交互之间没有关于游戏状态读取的竞争条件?

    我知道模拟的结果(数据量很小)可以通过swingutilities.invokelater()调用有效地移回EDT。

    游戏状态模型似乎过于复杂,不可能只在任何地方使用不可变的值类。

    是否有一种相对正确的方法来消除这种读取竞争条件?也许在每一个计时器上进行完整/部分的游戏状态克隆,或者将游戏状态的生存空间从EDT更改为其他线程?

    更新: (根据我的评论) 该游戏有13个人工智能控制玩家,1个人类玩家,拥有约10000个游戏对象(行星、建筑、设备、研究等)。例如,游戏对象具有以下属性:

    World (Planets, Players, Fleets, ...)
    Planet (location, owner, population, type, 
        map, buildings, taxation, allocation, ...)
    Building (location, enabled, energy, worker, health, ...)

    在一个场景中,用户在这个星球上建造了一座新的建筑。这在EDT中执行,因为需要更改地图和建筑物集合。与此平行的是,每500毫秒进行一次模拟,以计算所有游戏行星上建筑物的能量分配,这些建筑物需要穿过建筑物收集进行统计收集。如果计算分配,则将其提交给EDT,并分配每个建筑的能量场。

    只有人机交互才具有这种特性,因为人工智能计算的结果无论如何都适用于EDT中的结构。

    通常,75%的对象属性是静态的,仅用于渲染。它的其余部分可以通过用户交互或模拟/AI决策进行更改。还可以确保,在前一个模拟/AI步骤写回所有更改之前,不会启动任何新的模拟/AI步骤。

    我的目标是:

    • 避免延迟用户交互,例如用户将建筑物放置在地球上,只有在0.5秒获得视觉反馈后
    • 避免用计算、锁等待等方式阻塞EDT。
    • 避免集合遍历和修改、属性更改的并发问题

    选项:

    • 细粒度对象锁定
    • 不可变集合
    • 易挥发场
    • 部分快照

    所有这些对模型和游戏都有好处、缺点和原因。

    更新2: 我说的是 this 游戏。我的克隆是 here . 屏幕截图可能有助于想象渲染和数据模型交互。

    更新3:

    我将尝试给出一个小代码示例,以澄清我的问题,因为它似乎是从被误解的评论中得出的:

    List<GameObject> largeListOfGameObjects = ...
    List<Building> preFilteredListOfBuildings = ...
    // In EDT
    public void onAddBuildingClicked() {
        Building b = new Building(100 /* kW */);
        largeListOfGameObjects.add(b);
        preFilteredListOfBuildings.add(b);
    }
    // In EDT
    public void paint(Graphics g) {
        int y = 0;
        for (Building b : preFilteredListOfBuildings) {
            g.drawString(Integer.toString(b.powerAssigned), 0, y);
            y += 20;
        }
    }
    // In EDT
    public void assignPowerTo(Building b, int amount) {
        b.powerAssigned = amount;
    }
    // In simulation thread
    public void distributePower() {
        int sum = 0;
        for (Building b : preFilteredListOfBuildings) {
            sum += b.powerRequired;
        }
        final int alloc = sum / (preFilteredListOfBuildings.size() + 1);
        for (final Building b : preFilteredListOfBuildings) {
            SwingUtilities.invokeLater(=> assignPowerTo(b, alloc));            
        }
    }
    

    所以重叠在OnAddBuildingClicked()和DistributePower()之间。现在想象一下,在游戏模型的各个部分之间有50个这样的重叠。

    8 回复  |  直到 9 年前
        1
  •  3
  •   Atiaxi    15 年前

    这听起来似乎可以从客户机/服务器方法中获益:

    玩家是一个客户端-交互和渲染发生在这一端。所以播放器按下一个按钮,请求就转到服务器。服务器的回复返回,玩家的状态被更新。在这些事情发生的任何时候,屏幕都可以重新绘制,它反映出客户当前所知道的游戏状态。

    人工智能也是一个客户机——它相当于一个机器人。

    模拟是服务器。它在不同的时间从客户机获取更新并更新世界状态,然后根据需要将这些更新发送给每个人。这就是它与您的情况的联系:模拟/AI需要一个静态的世界,并且许多事情同时发生。服务器可以简单地将更改请求排队并应用它们,然后再将更新发送回客户端。就服务器而言,游戏世界并不是实时变化的,只要服务器决定了它的存在,游戏世界就会发生变化。

    最后,在客户端,您可以通过执行一些快速的近似计算并显示结果(从而满足即时需求),然后在服务器与您交谈时显示更正确的结果,来防止按下按钮和查看结果之间的延迟。

    请注意,这实际上不需要以TCP/IP的方式在Internet上实现,只是它有助于从这些方面考虑它。

    或者,您可以将在模拟过程中保持数据一致性的责任放在数据库上,因为它们已经在构建时考虑到了锁定和一致性。类似于sqlite的东西可以作为非网络解决方案的一部分工作。

        2
  •  0
  •   user101884    15 年前

    我不确定是否完全理解您要查找的行为,但听起来您需要类似于状态更改线程/队列的东西,因此所有状态更改都由单个线程处理。

    为状态更改队列创建一个API,可能类似于swingutilities.invokelater()和/或swingutilities.invokeAndWait(),以处理状态更改请求。

    我认为这在GUI中的反映方式取决于您正在寻找的行为。也就是说,由于当前状态为$0,所以无法提取资金,或者在处理提取请求时,向用户弹出帐户为空的消息。(可能不是用这个术语;-)

        3
  •  0
  •   Tom Hawtin - tackline    15 年前

    最简单的方法是使模拟足够快以在EDT中运行。喜欢有效果的节目!

    对于双线程模型,我建议将域模型与渲染模型同步。呈现模型应该保留来自域模型的数据。

    更新:在模拟线程中锁定渲染模型。遍历渲染模型更新,其中内容与预期不同,请更新渲染模型。完成遍历后,解锁渲染模型并计划重新绘制。注意,在这种方法中,您不需要大量的听众。

    渲染模型可以有不同的深度。在一个极端,它可能是一个图像,而更新操作只是用新的图像对象替换一个引用(例如,这不会很好地处理大小调整或其他表面交互)。您可能不需要检查项目是否有更改,只需要更新所有内容。

        4
  •  0
  •   Kathy Van Stone    15 年前

    如果更改游戏状态很快(一旦知道要将其更改为什么),则可以将游戏状态视为其他摆动模型,并且只更改或查看EDT中的状态。如果改变游戏状态不是很快,那么您可以同步状态改变并在Swing Worker/Timer(而不是EDT)中进行,也可以在单独的线程中进行,该线程与EDT处理类似(此时您将使用 BlockingQueue 处理变更请求)。如果用户界面不必从游戏状态中检索信息,而是通过侦听器或观察器发送呈现更改,那么最后一个更有用。

        5
  •  0
  •   Aaron    15 年前

    是否可以增量更新游戏状态,并且仍然有一个一致的模型?例如,在渲染/用户更新之间重新计算行星/玩家/舰队对象的子集。

    如果是这样,您可以在EDT中运行增量更新,在允许EDT处理用户输入和呈现之前,只计算状态的一小部分。

    在EDT中进行每次增量更新之后,您需要记住还有多少模型需要更新,并在执行任何挂起的用户输入和呈现之后,在EDT上安排一个新的swingworker来继续此处理。

    这样可以避免复制或锁定游戏模型,同时保持用户交互的响应性。

        6
  •  0
  •   mnuzzo    15 年前

    我认为你不应该让世界存储任何数据或对任何对象本身进行更改,它应该只用于维护对对象的引用,当需要更改该对象时,让玩家直接进行更改。在这种情况下,你唯一需要做的就是同步游戏世界中的每个对象,这样当一个玩家做出改变时,其他玩家就不能这样做了。下面是我想的一个例子:

    玩家A需要了解一个星球,所以它向世界请求那个星球(如何依赖于你的实施)。world返回一个对a请求的行星对象播放器的引用。玩家A决定做一个改变,所以它是这样做的。假设它增加了一个建筑物。将建筑物添加到行星的方法是同步的,因此一次只能有一个玩家这样做。这座建筑将跟踪自己的建造时间(如果有的话),所以地球的“添加建造方法”几乎可以立即释放。这样,多个玩家可以同时在同一个星球上请求信息,而不会相互影响,玩家几乎可以同时添加建筑,而不会出现太多延迟。如果两个玩家正在寻找一个放置建筑的地方(如果这是你游戏的一部分),那么检查一个位置的适合性将是一个查询而不是一个变更。

    很抱歉,如果这不能回答你的问题,我不确定我是否理解正确。

        7
  •  0
  •   Hans Malherbe    15 年前

    如何实现管道和过滤器体系结构?如果过滤器不够快,管道会将过滤器连接在一起并对请求进行排队。处理发生在过滤器内部。第一个过滤器是人工智能引擎,而渲染引擎由一组后续过滤器实现。

    在每一个计时器刻度上,新的动态世界状态是基于所有输入(时间也是一个输入)和 复制 插入到第一个管道中。

    在最简单的情况下,渲染引擎是作为一个过滤器实现的。它只是从输入管道获取状态快照,并将其与静态状态一起呈现。在实况游戏中,如果管道中有多个状态,渲染引擎可能希望跳过这些状态,而如果正在进行基准测试或输出视频,则希望渲染每个状态。

    将渲染引擎分解为的过滤器越多,并行性越好。也许甚至可以分解人工智能引擎,例如,您可能希望将动态状态分为快速变化和缓慢变化状态。

    这种体系结构在没有大量同步的情况下提供了良好的并行性。

    这种体系结构的一个问题是,垃圾收集将经常运行,每次都冻结所有线程,这可能会扼杀多线程带来的任何优势。

        8
  •  0
  •   nojevive    15 年前

    看起来您需要一个PriorityQueue来放置对模型的更新,在这个队列中,用户对来自模拟和其他输入的更新具有优先权。我听到你说的是,当其他输入(模拟,否则)可能需要比一个模拟步骤更长的工作人员时,用户总是需要立即对其操作进行反馈。 然后在PriorityQueue上同步。