为了学习Java,我正在创造一个经典的“空间飞行”#34;第二场比赛。除了玩家对象之外,还存在许多敌人/障碍物(清道夫,猎人,彗星,小行星),每个都有自己的类,扩展了GameObject类。由于可能有几种清道夫,彗星等,它们存储在arraylists中。然而,由于每个对象可以相互交互,因此存在许多循环和重复代码,例如每个外星人根据彗星arraylist,小行星数组列表中的对象等进行交互。
在我的游戏更新功能中,我有:
public void update() {
ArrayList<Rock> rockList = rock.getRockList();
ArrayList<Scavenger> scavengerList = scavenger.getScavengerList();
ArrayList<Hunter> hunterList = hunter.getHunterList();
....
npc.update(player, scavengerList, hunterList, rockList);
...
}
并在我的NPC类中(扩展了GameObject类)
public void update(Player player, ArrayList<Scavenger> scavengerList, ArrayList<Hunter> hunterList, ArrayList<Rock> rockList) {
for(int i = 0; i < scavengerList.size(); i++) {
scavengerList.get(i).update(player,scavengerList, ,hunterList rockList);
}
for(int i = 0; i < hunterList.size(); i++) {
hunterList.get(i).update(player,scavengerList, hunterList, rockList);
}
...
}
最后我在我的清道夫类,我的猎人班等中有一个更新功能,如
public class Hunter extends NPC{
...
public void update(Player player,ArrayList<Scavenger> scavengerList, ArrayList<Hunter> hunterList, ArrayList<Rock> rockList) {
"update all hunter objects according to the player, scavengers, rocks etc"
}
这种方法看起来相当麻烦,而且随着更多类的创建,需要解析和循环的数字或者arraylists失控。 任何人都可以推荐更好的方法吗? 我想显而易见的方法是让一个列表包含所有NPC对象,然后跟踪它们的类类型并相应地更新。 这是一种更好的方式,还是有人能指出我正确的方向?
答案 0 :(得分:2)
是的,有更好的方法。
对于游戏中的每种类型的对象,计算出它需要展示的一组行为/特征。应将这些行为定义为接口。然后处理行为/特征的代码可以使用接口,而不必知道任何关于实际类的内容。
例如,如果某些物体根据当前速度每次转动并且可能与其他物体发生碰撞,则可能存在界面:
public interface Moving {
void move();
boolean hasCollided(Shape shape);
void handleCollision(Collision collision);
}
任何移动的类都将实现此接口。然后World
对象可以有List<Moving> movingObjects
,然后使用:
movingObjects.forEach(Moving::move);
使用update
方法。
要在移动后处理碰撞,您可能会遇到以下情况:
List<Collision> collisions = getAllCollisions(movingObjects);
for (Collision collision: collisions) {
for (Moving element: collision.getCollidingObjects) {
element.handleCollision(collision);
}
}
如果实现接口的几个类使用类似的机制来移动自己,那么你应该将该逻辑移动到一个单独的类中:
class Inertia implements Moving {
private Velocity velocity;
@Override
public void move(Shape shape) {
velocity.applyTo(shape);
}
@Override
public void applyForceFrom(Position position) {
velocity.accelerateAwayFrom(position);
}
}
然后,您的世界对象可以将其移动行为委托给此类:
class Asteroid implements Moving {
private final Inertia inertia;
private Shape shape = new Circle(radius);
@Override
public void move() {
inertia.move(shape);
}
@Override
public boolean hasCollided(Shape other) {
return this.shape.intersects(other);
}
@Override
public void handleCollision(Collision collision) {
intertia.applyForceFrom(collision.getCentreOfMass());
}
}
这似乎是一种不必要的间接,但经验表明,从长远来看这是值得的。有关详细信息,请参阅Delegation Pattern。
如果每个对象的移动不同(例如,一些受重力影响,一些受AI控制等),或者一个类可以在其move
中应用多个代表(例如重力和惯性),则可以有多个代表或者如果一个类的行为是唯一的,那么它可以实现自己的move
。所有这一切都可以在World
不需要知道任何关于对象的类正在调用move
的情况下发生。
作为一般规则,尽量避免使用extends
来继承超类的行为。你的Hunter扩展NPC扩展GameObject的结构将很方便,直到你意识到你也希望Hunter扩展敌人或AIControlled或其他东西。硬性体验已经向OO编程人员表明,这些类型的层次结构最初看起来既合理又优雅,但随着您添加更复杂的功能而变得无法管理。
更进一步,隐藏哪些对象实现World
哪些行为接口的所有细节,您可能希望查看Visitor Pattern。这将允许世界对象作为移动者访问所有游戏对象,然后作为AIAgent访问,然后作为用户控制的对象等等,而无需知道他们在访问期间做了什么(或者如果他们做任何事情)。如果应用得很好,它会非常强大,但需要一点时间习惯。
最后,游戏编写者使用了一种非常常见的架构模式Entity Component System。如果您只是学习Java,我暂时忽略了这一点,但如果您成为一名认真的游戏开发人员,您可能会发现它比我上面描述的架构有所改进。
我显然在示例中遗漏了很多细节(例如Shape
,Circle
,Position
,Velocity
,{{1}的定义等等)但这是一般的想法。还有更多内容,值得寻找面向对象设计的书籍或教程,以便更深入地了解。