在高帧率贪吃蛇游戏中,我应该如何跟踪片段的移动?



更传统的蛇游戏中,每个分段都会取代前一个位置前的分段。问题是,我正在编程一个"平滑"的蛇游戏,因为每帧的头部都在以分钟为单位移动。

以前,我有turningPoint,它们是在头部移动时创建的。当前面的每个分段都到达该点时,它们将沿存储在turningPoint中的方向旋转。这样做的问题是,我必须不断地为每个turningPoint的每个片段绑定,这是一个累积的错误,因为移动不是完全平滑的(总是有可能片段完全错过了点,没有转向),而且片段不是"连接的",而是很多碰巧挨着移动的物体。

我的问题是:我该如何对一条能有效转弯的平滑蛇进行编程?

我假设您对模型(即位置/旋转数据的后端表示)有问题,而不是视图的问题(屏幕上显示的内容),因为通常平滑的模型移动可以很好地转换为平滑的视图移动。

事实上,这听起来像是函数/懒惰评估编程的完美场所。。。

首先,位置/方向:
(请注意,这里的Vector不是集合类,而是与图形编程中大量使用的数学概念有关)

public final class PositionAndOrientation {
    public final Vector position;
    public final Vector orientation;
    public PositionAndOrientation(Vector position, Vector orientation) {
        this.position = position;
        this.orientation = orientation;
    }
    public PositionAndOrientation move(Vector direction) {
        return new PositionAndOrientation(position.add(direction), orientation);
    }
    public PositionAndOrientation rotate(Vector degrees) {
        return new PositionAndOrientation(position, orientation.add(degrees));
    }
}

调整/更新界面:

public interface AdjustPositionAndOrientation {
    PositionAndOrientation adjust(long stamp);
}

起始位置:

public final class StartingPositionAndOrientation 
                                      implements AdjustPositionAndOrientation {
    private final PositionAndOrientation starting;
    public StartingPositionAndOrientation(final PositionAndOrientation start) {
        starting = start;
    }
    public final PositionAndOrientation adjust(long stamp) {
        return starting;
    }
}

移动段:

public final class MovePosition implements AdjustPositionAndOrientation {
    private final AdjustPositionAndOrientation previous;
    private final Vector direction;
    private final long start;
    public MovePosition(long start, Vector direction, 
                                    AdjustPositionAndOrientation previous) {
        this.start = start;
        this.direction = direction;
        this.previous = previous;
    }
    public final PositionAndOrientation adjust(long stamp) {
        return previous.adjust(Math.min(stamp, start)).move(
                  direction.multiply(Math.max(stamp - start, 0)));
    }
}

旋转段:

public final class RotatePosition implements AdjustPositionAndOrientation {
    private final AdjustPositionAndOrientation previous;
    private final Vector degrees;
    private final long start;
    public MovePosition(long start, Vector degrees, 
                                    AdjustPositionAndOrientation previous) {
        this.start = start;
        this.degrees = degrees;
        this.previous = previous;
    }
    public final PositionAndOrientation adjust(long stamp) {
        return previous.adjust(Math.min(stamp, start)).rotate(
                  degrees.multiply(Math.min(Math.max(stamp - start, 0), 90)));
    }
}

最后是细分市场本身:

public final class Segment {
    private final Segment parent;
    private final long offset;
    private final AdjustPositionAndOrientation place;
    private Segment (Segment parent, long offset, AdjustPositionAndOrientation place) {
        this.parent = parent;
        this.offset = offset;
        this.place = place;
    }
    public static Segment startingAt(AdjustPositionAndOrientation place) {
        return new Segment(null, 0, place);
    }
    public static Segment connectTo(Segment parent, long offset) {
        return new Segment(parent, offset, null);
    }     
    public final PositionAndOrientation getPositionAndOrientation(long elapsed) {
        if (parent != null) {
            return parent.getPositionAndOrientation(elapsed - offset);
        } else {
            return place.adjust(elapsed);
        }
    }
}

当然,这只是一个粗略的草图(没有经过测试),但基本的想法应该会让你开始。值得注意的是,无论何时向链中添加新的Adjust,都需要一个复制构造函数/工厂来重建段。然而,这种方法的美妙之处在于,我可以通过向父段传递最终(结束)Adjust实例来创建"重播"。

(注意:我使用了long作为游戏时钟,因为Date就是这样使用的。但是,考虑到int可以在内保持足够的毫秒数,您可以将其交换)。


编辑:

要想"移动"蛇,你实际上需要复制。在Segment中添加这样的方法就可以了:

public final Segment addAdjutment(AdjustPositionAndOrientation place) {
    if (parent != null) {
        return connectTo(parent.addAdjustment(place), offset);
    } else {
        return startingAt(place);
    }
}

并在尾部调用该方法,而不是在头部调用(顺便说一句,你只需要维护对蛇尾部的外部引用,这使得添加额外的段变得微不足道——尽管我认为这可以逆转)。请注意,无论你对方法进行什么调整,都应该已经封装了"驱动"蛇的方法,否则你需要在接口中添加某种copy(..)方法。这意味着您需要保留对当前调整的外部引用,或者向snake添加递归get()方法(实现起来很简单)。

根据事物的声音,你预计每个片段都会在几帧后回溯头部所经历的点。在这种情况下,只需让你的蛇数据结构成为一个头部所在点的列表……然后要绘制蛇,在该列表中循环,跳过每10个点,然后在那里绘制一段。

最新更新