Java:获取资源和得墨忒耳法则

Java: Accessing resources and the Law Of Demeter

概览

在我的 (Android) Java 游戏中,我有一个名为 resources 的 class。顾名思义,这个 class 保存着游戏的资源。我所有的 OpenGL 对象(Sprites)都是在这里创建的

它看起来像下面这样(显然,与实际项目中出现的相比,这是一个简化版本):

public class Resources {

    Hero hero;
    Enemy enemy;
    MenuButtons mainMenuButtons;
    Background background;
    Scene mainMenu;

    public void createObjects(){

        hero = new Hero();
        enemy = new Enemy();
        mainMenuButtons = new MenuButtons();
        background = new Background();
        mainMenu = new Scene(this);

    }
}

因此,在我的 mainMenu 场景中,我需要访问我的对象,因此我们可能会看到类似这样的内容:

public class mainMenu implements Scene {

    Resources resources;

    public mainMenu(Resources resources){

        this.resources = resources;

    }

    @Override
    public void render(){

        resources.background.draw();
        resources.hero.draw();
        resources.enemy.draw();
        mainMenuButtons.draw();           

    }

    @Override
    public void updateLogic(){

        resources.hero.move();
        resources.enemy.move();
        resources.mainMenubuttons.animate();
    }

}

现在,上述方法只是访问 resources and 中对象的一种方法。但这是否真的违反了 Demeter 法则? 如果不是,为什么不呢?如果是这样,以不违反 LOD 的方式访问这些对象的最佳方式是什么?

访问器?

一个选项(我已经排除了 TBH - 见下文)是将访问器方法放入我的 资源 class。这样我就可以做类似的事情:

resources.drawBackround();

我有很多 个对象,每个对象的每个method/variable 都需要一个访问器。不太实用,似乎我正在编写大量额外代码,最重要的是,它使 resources class 变得荒谬地长,因为它充满了这些访问器。所以,我不会走这条路。

将对象传入场景的构造函数

当然,我也可以这样做:

    hero = new Hero();
    enemy = new Enemy();
    mainMenuButtons = new MenuButtons();
    background = new Background();
    mainMenu = new Scene(hero, enemy, mainMenuButtons, background);

所以我可以简单地这样做:

    background.draw(); //etc....

这对于简单场景(例如不需要很多对象的菜单系统)是可行的,但对于主要游戏,它很快就会变得一团糟,因为我必须传递对 30 多个对象的引用将对象放入构造函数中,这听起来不太正确......

所以,如果有人能指出最好的方法和原因,我将不胜感激。

您可以使用依赖注入并消除您的资源 class。然后你可以在你的字段上调用函数并且不会违反 Demeter 法则。

下面是一个使用构造函数注入的例子:

public class MainMenu implements Scene {

   Background background;
   Hero hero;  
   Enemy enemy; 
   MenuButtons buttons

    public mainMenu(Background background, Hero hero,  Enemy enemy, MenuButtons buttons){

       this.background = background;
       this.hero = hero;
       this.enemy = enemy;
       this.buttons = buttons;   
    }

    @Override
    public void render(){

        this.background.draw();
        this.hero.draw();
        this.enemy.draw();
        this.mainMenuButtons.draw();           

    }

    @Override
    public void updateLogic(){

        this.hero.move();
        this.enemy.move();
        this.mainMenubuttons.animate();
    }

}

通过依赖注入,您可以将实例传递给构造函数和函数,而不是 "newing" 它们在您的 class 中。不过,您需要在某个地方管理您的实例,并且有很多库可以为您做到这一点。 Dagger 是 Android 的热门产品:http://square.github.io/dagger/

So I would really appreciate if someone could point out the best way to proceed and why.

在我看来,最好的方法是保留资源 class,将所有对象设为私有,以免违法并写入访问器(但不是像您已经排除的每个对象)。

I have a lot of objects and I need an accessor for each method/variable of each object. Not really practical, it seems like I'm writing a ton of extra code and most importantly, it makes the resources class ridiculously long as it becomes filled with these accessors. Therefore, I'm not going down this road.

我假设很多对象都是相同的class。所以你不必为每个对象都创建一个访问器,这真的会破坏 class。 在一款游戏中,您通常有一个英雄、一个或多个敌人和许多精灵。

public class Resources {

    private Hero hero;
    private Enemy enemy;
    private MenuButtons mainMenuButtons;
    private Background background;
    private Scene mainMenu;

    public void createObjects(){

        hero = new Hero();
        enemy = new Enemy();
        mainMenuButtons = new MenuButtons();
        background = new Background();
        mainMenu = new Scene(this);

    }

    public Hero getBackground() {

        return background;
    }

    public Hero getHero() {

        return hero;
    }

    public List<Enemy> getEnemies() {

        ArrayList<Enemy> list = new ArrayList<Enemy>();
        list.add(enemy);
        list.add(next_enemy);
        return list;
    }

    public List<Sprite> getSprites() {

        ArrayList<Sprite> list = new ArrayList<Sprite>();
        list.addAll(enemy.getActiveSprites());
        return list;
    }

}

如果 Hero 和 Enemy 派生自同一个 class,您也可以创建 getActor() 方法而不是 getHero() 和 getEnemy()。 getSprites() 方法只是一个示例。

如果该解决方案不适合您,我还有另一个建议。

使资源 class 做一些工作。

public class ResourceManager {

    private Hero hero;
    private Enemy enemy;
    private MenuButtons mainMenuButtons;
    private Background background;
    private Scene mainMenu;

    public void createObjects(){

        hero = new Hero();
        enemy = new Enemy();
        mainMenuButtons = new MenuButtons();
        background = new Background();
        mainMenu = new Scene(this);

    }

    public void render(Scene scene) {

        this.background.draw();
        if (scene != mainMenu) {

            this.hero.draw();
            this.enemy.draw();
        }
        this.mainMenuButtons.draw();           

    }

    public void updateLogic(Scene scene){

        this.hero.move();
        this.enemy.move();
        this.mainMenubuttons.animate();
    }

}

然后mainMenu直接调用RescourceManager中的逻辑方法class。

public class mainMenu implements Scene {

    ResourceManager resourceManager;

    public mainMenu(ResourceManager resourceManager){

        this.resourceManager = resourceManager;
    }

    @Override
    public void render(){

        resourceManager.render(this);
    }

    @Override
    public void updateLogic(){

        resourceManager.updateLogic(this);
    }

}

我希望我的建议能帮助您了解如何继续您的项目。

传递列表的想法是不错的第一步,但还不够。游戏开发人员有一个(有点争议的)结构概念,称为 "scene graph" 可以帮助他们跟踪他们的资源(以及其他事物)。 https://en.wikipedia.org/?title=Scene_graph

这是一个相当复杂的概念,但您迟早需要了解它。 gamedev.stackexchange.com上有很多好的建议,建议你去那边看看。

这是关于该主题的精彩 YouTube 视频教程。 https://www.youtube.com/watch?v=ktz9AlMSEoA

您可以创建一个 Drawer class 来处理所有对象的绘制。您的场景对象只需要向 Drawer 提供我假设为 Drawable.

的对象
public class Drawer {
   public void drawObjects(Drawable... objects) {
      for(Drawable drawable : objects) {
         drawable.draw();
      }
   }
}

然后 Scene 使用它来绘制这些对象。

public class mainMenu implements Scene {
   Resources resources;
   Drawer drawer;

   ...

   public void render() {
      drawer.drawObjects(resources.background, 
                         resources.hero, 
                         resources.enemy, 
                         resources.mainMenuButtons);
   }

   ...
}

类似的策略,使用 Updater,可以应用于其他方法。如果您的 updateLogic() 方法使调用像它看起来的那样简单,那么您绝对可以通过使所有这些对象继承自 Updateable 接口来做同样的事情。

public interface Updateable {
   void update();
}

HeroEnemyupdate() 方法可以简单地调用它们的 move() 方法,而 MenuButtonsupdate()可以委托给animate()

显然,如果您愿意,可以使用某种集合而不是可变参数作为 DrawerdrawObjects() 的参数。我只是喜欢 varargs 带来的流畅性,因为您不必创建集合。

有关保持代码符合 Demeter 法则的其他技巧,请查看这篇文章:Law of Demeter and How to Work With It

改变这个:

 public void render(){

        resources.background.draw();
        resources.hero.draw();
        resources.enemy.draw();
        mainMenuButtons.draw();           

    }
 @Override
    public void updateLogic(){

        resources.hero.move();
        resources.enemy.move();
        resources.mainMenubuttons.animate();
    }

有了这个:

public void render(){
            resources.render();
    } 

@Override
    public void updateLogic(){
        resources.update();
} 

ResourceManager 不必知道 Resources 里面有什么。可能是一个敌人,也可能是十个敌人,与ResourceManager无关。

所以在 'Resource' 你可以这样做:

  public void update(){
        hero.update();// Cause hero may, or may not move, he makes the choice
        enemy.update();//...
        mainMenubuttons.update();//.
    }
  public void render(){
  ...
  }

不止于此!您可以使用接口更改 "Resource" 实现,您将为接口而不是实现编程,这很酷!这样你就可以有一个 'Resources' 用于游戏内,另一个用于菜单,它们将以相同的方式使用:仅在运行时更改你将在菜单或游戏中的具体资源!

总之,得墨忒耳并不总是需要填满的

我喜欢 ResourceManager 的概念。
但是 ResourceManager 应该负责加载资源、缓存和释放它们。
渲染绝对是渲染对象的一种方法。

因此 Scence - render 方法可以在
实例化渲染器后将渲染委托给它,并用 Drawables 提供给它,因为渲染器不
渲染资源而是渲染可渲染对象。

说:

class MainMenu implements Scene {
    Renderer sceneRenderer = new Renderer();
    AnimatedRenderer animatedRenderer = new AnimatedRenderer();
    ResourceManager resourceManager = ResourceManager.getInstance();
    List<Resource> resources;
    List<Drawable> renderedObjects;
    GameObjectController gameObjectController;


    void initializeScene() {
          resources = resourceManager.getResources();
          renderedObjects = resourcesAsRenderables();
          sceneRenderer.setDrawables(renderedObjects);
    }

    List<Drawable> resourcesAsRenderables() {
      // if resources are not directly renderable, do decoration etc
      // and return a List of Drawable
    }

    @Override
    public void render(){
         sceneRenderer.render();
    }
    @Override
    public void updateLogic(){
       moveGameObjects();
       doAnimations();

    }
    protected void moveGameObjects() {
        gameObjectController.moveAllObjects(this, resources);
    }
    protected void doAnimations() {
        animatedRenderer.render(resources);
    }


    class ResourceManager {
       private static ResourceManager instance = null;
       List<Resource> resources;
       public ResourceManager getInstance() {
          if(instance == null) {
             instance = new ResourceManager();
             instance.loadResources();
          }
          return instance;
       }
       private void loadResources() { 
           resources = new LinkedList<Resource>();
           resources.add(new Hero());
           ....
       }
       public List<Resource> getResources() {
          return resources;
       }
    }

这清楚地分离了场景生命周期中执行的任务的逻辑和责任。负责检索资源的资源管理器,因为它们可能需要很长的加载时间,所以在低内存情况下会执行诸如缓存或释放之类的操作,从而向客户端隐藏详细信息。渲染器负责显示对象,控制器负责移动对象。控制器本身可以实现键盘事件的处理程序,但这不是必须对场景透明的东西。渲染器可以将背景调入或调出或缩放或设置光照效果,但场景仅调用其渲染方法。动画渲染器负责启动、渲染和停止动画。

可以看出您的资源不需要重新创建,相反它们确实使用了一些无法重新加载的资源(可能是图像)。

您应该在资源 class 中共享图像对象,并在场景 class 中创建对象,在实体的构造函数中您可以获得预加载的共享资源.