Наблюдаемое исключение ConcurrentModification - PullRequest
0 голосов
/ 26 мая 2018

У меня проблема с шаблоном проектирования наблюдателя.

Я попытался реализовать что-то вроде Наблюдателей самостоятельно и столкнулся с неприятной ошибкой, которую я не понимаю.

java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(Unknown Source)
at java.util.ArrayList$Itr.next(Unknown Source)
at userInterface.PauseScreen.acknowledgeChanges(PauseScreen.java:57)
at userInterface.PauseScreen$1.getPressed(PauseScreen.java:22)
at evo.EvoMain.update(EvoMain.java:76)
at org.newdawn.slick.GameContainer.updateAndRender(GameContainer.java:646)
at org.newdawn.slick.AppGameContainer.gameLoop(AppGameContainer.java:412)
at org.newdawn.slick.AppGameContainer.start(AppGameContainer.java:322)
at evo.EvoMain.main(EvoMain.java:32)
Sat May 26 17:28:31 CEST 2018 ERROR:Game.update() failure - check the game code.
org.newdawn.slick.SlickException: Game.update() failure - check the game code.
at org.newdawn.slick.GameContainer.updateAndRender(GameContainer.java:663)
at org.newdawn.slick.AppGameContainer.gameLoop(AppGameContainer.java:412)
at org.newdawn.slick.AppGameContainer.start(AppGameContainer.java:322)
at evo.EvoMain.main(EvoMain.java:32)

Предыстория: я программирую игру на Java в Slick2D, и она движется в направлении Diablo.Когда я услышал о шаблоне проектирования наблюдателя, я был взволнован и попытался внедрить его в свою игру, чтобы различные компоненты могли легко взаимодействовать друг с другом.Мне бы хотелось, чтобы экран «Пауза» связывался с основным классом игры.Я также хотел бы, чтобы основной класс связывался с экраном статуса игрока.Эти два канала связи предназначены главным образом для того, чтобы сообщить тем компонентам, какой ввод был сделан, и получить кнопку, которую они нажали в качестве обратной связи.Я также хотел бы, чтобы statscreen общался с игроком.1-й: я хочу, чтобы экран статистики сообщал игроку, какую статистику он должен улучшить, и 2-й: я хочу, чтобы игрок возвращал свою статистику (hp, hpRegeneration, stamina, staminaRegen, dmg) на экран статистики, чтобы пользователь мог посмотреть на него.их там и читайте цифры.Я думаю, что это можно было бы реализовать без наблюдателей, но у меня были трудности, и я подумал, что в конечном итоге было бы лучше реализовать наблюдателей и сохранить объекты независимыми таким образом.

Итак, чтобы показать вам несколько фотографий:

Стартовый экран

В этот момент пользователь нажимает кнопку Старт, и игра инициализируется.Монстры и игрок загружены и все остальное.

К сожалению, кнопка «Пуск» выдает странную ошибку ConcurrentModificationError, когда пытается сообщить игре, что она нажата.Что еще более странно для меня, так это тот факт, что ошибка возникает только в том случае, если я добавляю игрока на его / ее statScreen в качестве слушателя, что для меня не имеет смысла, это кажется таким не связанным.

Строка, которая вызывает ошибку.Если я деактивирую строку, игра работает отлично.

Редактировать: Иногда, когда я ставлю точку останова на строку кода, где происходит ошибка, эта строка срабатывает 3 раза до появления исключения.На первых 2 перерывах ArrayList наблюдателей содержит EvoMain, на третьем, однако, он также содержит игрока.Это почему? Игрок является одним из наблюдателей PauseScreen, почему? Когда я удаляю строку кода, упомянутую выше выше Строка, которая вызывает ошибку.Если я деактивирую линию, игра работает отлично. Игрок не появляется в ArrayList и игра начинается нормально.Может быть, какая-то ошибка приводит к тому, что ArrayList используется совместно для объектов pauseScreen и statScreen?Это кажется статичным ?!Конец редактирования.

Я удалил большую часть реального игрового контента из этой версии игры, чтобы изолировать неисправный код.Вот мои классы:

EvoMain:

package evo;

import java.util.Hashtable;
import java.util.Map;
import org.lwjgl.input.Mouse;
import org.newdawn.slick.*;
import userInterface.EPauseScreenMessage;
import userInterface.PauseScreen;
import userInterface.StatScreen;

public class EvoMain extends BasicGame implements Observer {

private static Map<String, Image> imageCatalogue = new Hashtable<String, Image>();
private static Player player = null;
private static PauseScreen pauseScreen = null;
private static StatScreen statScreen = null;
private static GameContainer container;

private static int state = 0;   // 0 = paused, 1 = running

public EvoMain() throws SlickException {
    super("Evo");
}

public static void main(String[] args) throws SlickException {
    AppGameContainer container = new AppGameContainer(new EvoMain());
    container.setDisplayMode(1200, 900, false);
    container.start();
}

@Override
public void render(GameContainer container, Graphics g) throws SlickException {
    if (player != null) {
        player.render(container, g);
        statScreen.render(container, g);
    }
    pauseScreen.render(container, g);
}

@Override
public void init(final GameContainer container) throws SlickException {
    fillImageCatalogue();
    EvoMain.container = container;
    pauseScreen = new PauseScreen(200, 100);
    pauseScreen.add(this);
    pauseScreen.setVisible(true);
    pauseScreen.setEnabled(true);
    statScreen = new StatScreen(0, 0, imageCatalogue.get("PlusButton"));
    statScreen.add(this);
    container.setMinimumLogicUpdateInterval(55);
    container.setMaximumLogicUpdateInterval(55);
}

@Override
public void update(GameContainer container, int delta) throws SlickException {
    Input input = container.getInput();
    switch (state) {
    case 0:
        if(input.isKeyPressed(Input.KEY_ESCAPE)){
            resumeGame();
            break;
        }
        if (input.isKeyPressed(Input.KEY_ENTER)) {
            resumeGame();
            break;
        }
        if (input.isMouseButtonDown(0)) {
            if (pauseScreen.isVisible() && pauseScreen.isEnabled()) {
                for (Button button : pauseScreen.getButtons()) {
                    if (button.getRect().contains(Mouse.getX(), container.getHeight() - Mouse.getY())) {
                        // for some weird reason Mouse.getY() always gives the container height - the mouse' y coord so I have to reverse that by doing the same
                        button.getPressed();
                    }
                }
            }
        }
        break;  // end of pause state
    case 1:
        if(input.isKeyPressed(Input.KEY_ESCAPE)){
            state = 0;
            pauseScreen.setVisible(true);
            pauseScreen.setEnabled(true);
            break;
        }
        if(input.isKeyPressed(Input.KEY_T)){
            if (statScreen.isEnabled()) {
                statScreen.setEnabled(false);
                statScreen.setVisible(false);
            } else {
                statScreen.setVisible(true);
                statScreen.setEnabled(true);
            }
        }
        int dX = -1; // player destination X
        int dY = -1; // player destination Y
        if (input.isMouseButtonDown(0)) {
            dX = Mouse.getX();
            dY = container.getHeight() - Mouse.getY();
            if (statScreen.isVisible() && statScreen.isEnabled()) {
                for (Button button : statScreen.getButtons()) {
                    if (button.getRect().contains(dX, dY)) {
                        button.getPressed();
                    }
                }
            }
        }
        if (player != null) {
            player.setDestination(dX, dY);
        }
        break; // end of running state
    }
}
public void resumeGame() {
    state = 1;
    pauseScreen.setVisible(false);
    pauseScreen.setEnabled(false);
}

public static void fillImageCatalogue() throws SlickException {
    imageCatalogue.put("PlusButton"         , new Image("res/PlusButton.jpg"));
}

public void initializeGame()  {
    EvoMain.player = new Player((float) 100, (float) 100);
    statScreen.add(EvoMain.player);
    // this line throws an exception in PauseScreen acknowledgeChanges();
}
public Player getPlayer() {
    return player;
}
public void setPlayer(Player player) {
    EvoMain.player = player;
}
@Override
public void update(Observable subject, Object object) {
    if (subject instanceof PauseScreen) {
        switch ((EPauseScreenMessage) object) {
        case EXIT:
            container.exit();
            break;
        case RESUME:
            resumeGame();
            break;
        case START:
            initializeGame();
            resumeGame();
        default:
            break;
        }
    } else
    if (subject instanceof StatScreen) {
        if (object == null) {
            statScreen.setVisible(false);
            statScreen.setEnabled(false);
        }
    }
}
}

класс Player

package evo;

import org.newdawn.slick.GameContainer;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.geom.Vector2f;
import userInterface.StatScreen;

public class Player extends Creature implements Observer, Observable {

private PlayerMessage pMessage = null;

private boolean enemyNearby             = false;

public Player(float x, float y) {
    super(x, y, 10.0, 9);
    setAttackDmg(1.0);
}

@Override
public void render(GameContainer container, Graphics g) throws SlickException {
}

@Override
public void update(EvoMain game) {

}

public void calculateBodyWeight() {

}
public void setDestination(int x, int y) {
    if (x != -1) {
        destinationVector = new Vector2f(x - getPosition().getX(),y - getPosition().getY());
        if (destinationVector.length() == 0.0) { // destination = player position ?
            destinationVector = null; // no need to move then
        }
    } else {
        destinationVector = null; // no destination active -> no vector needed
    }
}

public boolean dealDamage(Creature creature) {
    creature.setInvulnerableTicks(60);
    if (creature.getHp() <= 0) {
        kill(creature);
        return true;
    }
    return false;
}

public void die() {

}
public double getHp() {
    return hp;
}
public boolean hasEnemyNearby() {
    return enemyNearby;
}
public void setEnemyNearby(boolean enemyNearby) {
    this.enemyNearby = enemyNearby;
}
public void acknowledge(EStatMessage message, double value) {
    pMessage = new PlayerMessage(message, value);
    acknowledgeChanges();
}
@Override
public void acknowledgeChanges() {
    for (Observer o : observers) {
        o.update(this, pMessage);
    }
}
public void add(Observer newObs) {
    observers.add(newObs);
}
public void remove(Observer actObs) {
    observers.remove(actObs);
}
@Override
public void update(Observable subject, Object object) {
    if (subject instanceof StatScreen) {

    }
}

@Override
public void kill(Creature creature) {

}
}

Интерфейс Observer

package evo;

public interface Observer {

void update(Observable subject, Object object);

}

Интерфейс Observable

package evo;

import java.util.ArrayList;

public interface Observable {

ArrayList<Observer> observers = new ArrayList<Observer>();

public void acknowledgeChanges();
public void add(Observer newObs);
public void remove(Observer actObs);
}

Класс PauseScreen

package userInterface;

import org.newdawn.slick.Color;
import org.newdawn.slick.geom.Rectangle;

import evo.Observable;
import evo.Observer;

public class PauseScreen extends UserInterface implements Observable, Observer {

EPauseScreenMessage message;

public PauseScreen(float x, float y) {
    super(x, y, 300, 400);
    setMainColor(new Color(200, 200, 200));
    setBorderColor(new Color(150, 150, 150));
    TextButton btnStart = new TextButton(getX() + 20, getY() + 20, new Rectangle(getX() + 20, getY() + 20, 200, 75), "Start") {
        @Override
        public void getPressed() {
            if (isEnabled()) {
                message = EPauseScreenMessage.START;
                acknowledgeChanges();
            }
        }
    };
    add(btnStart);
    TextButton btnResume = new TextButton(getX() + 20, getY() + 120, new Rectangle(getX() + 20, getY() + 120, 200, 75), "Resume") {
        @Override
        public void getPressed() {
            if (isEnabled()) {
                message = EPauseScreenMessage.RESUME;
                acknowledgeChanges();
            }
        }
    };
    add(btnResume);
    TextButton btnExit = new TextButton(getX() + 20, getY() + 220, new Rectangle(getX() + 20, getY() + 220, 200, 75), "Exit game") {
        @Override
        public void getPressed() {
            if (isEnabled()) {
                message = EPauseScreenMessage.EXIT;
                acknowledgeChanges();
            }
        }
    };
    btnExit.setTextColor(Color.red);
    add(btnExit);   
}

@Override
public void update(Observable subject, Object object) {

}

@Override
public void acknowledgeChanges() {
    for (Observer obs : observers) {
        obs.update(this, message);
    }
}

@Override
public void add(Observer newObs) {
    observers.add(newObs);
}

@Override
public void remove(Observer actObs) {

}
}

Класс StatScreen

package userInterface;

import org.newdawn.slick.Color;
import org.newdawn.slick.Image;
import org.newdawn.slick.geom.Rectangle;
import evo.Observable;
import evo.Observer;
import evo.Player;
import evo.PlayerMessage;

public class StatScreen extends UserInterface implements Observer, Observable{

String stat = "1";

public StatScreen(float x, float y, Image plusButtonImage) {
    super(x, y, 500, 550);
    mainColor   = new Color(200, 200, 200);
    borderColor = new Color(150, 150, 150);
    TextButton btnClose = new TextButton(getX() + 280, getY() + 460, new Rectangle(getX() + 280, getY() + 460, 200, 75), "Close") {
        @Override
        public void getPressed() {
            if (isEnabled()) {
                stat = "2";
                acknowledgeChanges();
            }
        }
    };
    this.add(btnClose);
}

@Override
public void acknowledgeChanges() {
    for (Observer observer : observers) {
        observer.update(this, stat);
    }
}

@Override
public void add(Observer newObs) {
    observers.add(newObs);
}

@Override
public void remove(Observer actObs) {
    observers.remove(actObs);
}

@Override
public void update(Observable subject, Object object) {
    if (subject instanceof Player) {
        PlayerMessage pM = (PlayerMessage) object;
        switch (pM.getStat()) {
        default:
            break;
        }
    }
}
public String prepareValue(double value) {
    return (int) value + "." + (int) (value * 10);
}
}

Класс UserInterface

package userInterface;

import java.util.ArrayList;

import org.newdawn.slick.Color;
import org.newdawn.slick.GameContainer;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.SlickException;
import evo.Button;
import evo.EvoObject;

public abstract class UserInterface extends EvoObject {

boolean visible = false;
boolean enabled = false;
int width;
int height;
Color mainColor;
Color borderColor;
ArrayList<Button>   buttons = new ArrayList<Button>();

public UserInterface(float x, float y, int width, int height) {
    super(x, y);
    this.width = width;
    this.height = height;
}

public void render(GameContainer container, Graphics g) throws SlickException {
    if (visible) {  // render menu if visible
        g.setColor(mainColor);
        g.fillRect(getX(), getY(), this.width, this.height);
        g.setColor(borderColor);
        g.setLineWidth(3);
        g.drawRect(getX(), getY(), this.width, this.height);
        for (Button button : buttons) { // render all buttons
            button.render(container, g);
        }
    }
}

public boolean isVisible() {
    return visible;
}
public void setVisible(boolean visible) {
    this.visible = visible;
}
public boolean isEnabled() {
    return enabled;
}
public void setEnabled(boolean enabled) {
    this.enabled = enabled;
}
public int getWidth() {
    return width;
}
public void setWidth(int width) {
    this.width = width;
}
public int getHeight() {
    return height;
}
public void setHeight(int height) {
    this.height = height;
}
public Color getMainColor() {
    return mainColor;
}
public void setMainColor(Color mainColor) {
    this.mainColor = mainColor;
}
public Color getBorderColor() {
    return borderColor;
}
public void setBorderColor(Color borderColor) {
    this.borderColor = borderColor;
}
public void add(Button button) {
    buttons.add(button);
}
public ArrayList<Button> getButtons() {
    return buttons;
}
}

1 Ответ

0 голосов
/ 26 мая 2018

Проблема, вызванная неправильным использованием интерфейса.

public interface Observable {

    ArrayList<Observer> observers = new ArrayList<Observer>();

Вы объявляете список в интерфейсе Observable, следовательно, он становится static, совместно используемым между PauseScreen и StatScreen

Когда START, PauseScreen уведомляет ваших наблюдателей:

case START:
     initializeGame();
     resumeGame();

Обратите внимание, что вы итерируете PauseScreen наблюдателей с помощью итератора foreach, в этом case Start вы изменяете базовый список с помощью add new Player() к наблюдаемому списку в initializeGame методе:

public void initializeGame()  {
    EvoMain.player = new Player((float) 100, (float) 100);
    statScreen.add(EvoMain.player);

Это вызовет исключение одновременной модификации, когда iter.next вызывается в foreach цикле PauseScreen.acknowledgeChanges().

Решить эту проблему легко: удалите ArrayList<Observer> observers из вашего Observable интерфейса и создайте его в классе реализации (PauseScreen, StatScreen)

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...