Итак, ваша основная проблема может быть подытожена здесь ...
public class UIFrame extends JFrame {
//...
public UIFrame() {
setSize(size);
add(background);
add(userScreen);
С этим не так.Во-первых, JFrame
использует BorderLayout
для разметки своих компонентов, то есть фактически будет размечен только последний добавленный компонент.
См. Расположение компонентов в контейнере для получения более подробной информации.
Во-вторых, компоненты отображаются в порядке LIFO, поэтому в вашем случае это будет userScreen
затем background
- но поскольку background
никогда не выкладывается (его размер равен 0x0
), вы его не увидите.
Причина, по которой я упоминаю это, заключается в том, что вы не должны пытаться спроектировать себязапрограммируйте таким образом.
Следующий выпуск ...
public static Image[] scheme_player = new Image[1];
public static boolean imagesLoaded = false;
public static boolean leftPress = false;
public static boolean rightPress = false;
public static boolean upPress = false;
public static boolean downPress = false;
public static boolean mouseClick = false;
Это довольно хороший признак плохого дизайна.static
не ваш друг и может привести к очень интересному (и сложному для отладки) поведению
Вместо этого мы хотим думать о вашем дизайне более агностически.
Отказ от ответственности
Это «пример», предназначенный для предоставления «базовой линии» идей, из которых вы можете вырастить свой собственный API / концепцию.В этом примере есть некоторые вещи, которые я мог бы сделать по-другому, если бы разрабатывал собственное решение, но это было сделано для краткости и уменьшения общей сложности
Основные понятия
У вас есть вещи, которыекраски, у вас есть вещи, которые движутся, у вас, вероятно, есть вещи, которые сталкиваются и другие вещи.Вам нужен какой-то способ управлять и координировать все эти «вещи»
Основным принципом ООП (и программирования в целом) является концепция «модель-представление-контроллер».Идея состоит в том, чтобы разделить ваши концепции на более мелкие, не связанные между собой единицы работы, которые затем могут быть собраны воедино любым необходимым образом для формирования большой картины (или совокупности работ)
Итак, давайтеначать с основных строительных блоков, «сущность».«Сущность» - это нечто в вашей программе, которое несет информацию и может выполнять различные задачи и роли.Поскольку у вас может быть любое количество типов объектов, я бы начал с серии базовых интерфейсов, которые описывают базовое поведение различных типов объектов ...
public interface Entity {
// Other common properties
}
public interface MovableEntity extends Entity {
public void move(GameModel model);
// Other "movable" entities might also have
// speed adjustments, but I might consider those as
// seperate entities in of themselves, but that's me
}
public interface PaintableEntity extends Entity {
public void paint(Graphics2D g2d, GameModel model);
}
Обратите внимание, я не сделал подвижным илиможно нарисовать в одну сущность (вы можете создать четвертую interface
, которая это делает), потому что у вас может быть подвижная сущность, которая не закрашена, или закрашенная сущность, которая не движется, как фон!
Далее нам нужен какой-то контейнер для хранения важной информации, которая может понадобиться остальной части игры, чтобы выполнять свою работу, какая-то модель!
public interface GameModel {
public enum Input {
UP, DOWN, LEFT, RIGHT;
}
public boolean hasInput(Input input);
public Dimension getViewableArea();
// Other properties which might be needed by the
// entities
}
Это довольно просто, ноэто концепция, вокруг которой распространяется информация.Здесь я сосредоточился только на тех элементах, которые действительно нужны сущностям (нет необходимости раскрывать функциональность, которая им на самом деле не нужна).
Я также подготовил состояние для «ввода», которое является независимымот того, как этот вход на самом деле происходит.Остальным API это не важно, они хотят знать, доступен ли ввод.
С этого я могу начать строить некоторые базовые объекты ...
public class BackgroundEntity implements PaintableEntity {
@Override
public void paint(Graphics2D g2d, GameModel model) {
g2d.setColor(Color.BLUE);
Dimension bounds = model.getViewableArea();
g2d.fillRect(0, 0, bounds.width, bounds.height);
}
}
public class PlayerEntity implements PaintableEntity, MovableEntity {
private int speed = 2;
private int x, y;
public PlayerEntity() {
// load the player image
}
public int getSpeed() {
return speed;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public void setX(int x) {
this.x = x;
}
public void setY(int y) {
this.y = y;
}
@Override
public void paint(Graphics2D g2d, GameModel model) {
g2d.setColor(Color.RED);
g2d.drawRect(getX(), getY(), 10, 10);
}
@Override
public void move(GameModel model) {
int speed = getSpeed();
int x = getX();
int y = getY();
if (model.hasInput(GameModel.Input.UP)) {
y -= speed;
} else if (model.hasInput(GameModel.Input.DOWN)) {
y += speed;
}
if (model.hasInput(GameModel.Input.LEFT)) {
x -= speed;
} else if (model.hasInput(GameModel.Input.RIGHT)) {
x += speed;
}
Dimension bounds = model.getViewableArea();
if (y < 0) {
y = 0;
} else if (y + 10 > bounds.height) {
y = bounds.height - 10;
}
if (x < 0) {
x = 0;
} else if (x + 10 > bounds.width) {
x = bounds.width - 10;
}
setX(x);
setY(y);
}
}
Ладно, все хорошо, но нам нужен какой-то способ для обновления состояния, в этом случае мы разрабатываем концепцию изменчивой модели.Намерение заключалось в том, чтобы «скрыть» функциональность, поэтому мы не предоставляем его тем частям API, которые не нужны (игроку не нужно иметь возможность добавлять / удалять новые объекты)
public interface MutableGameModel extends GameModel {
public void setInput(Input input, boolean enabled);
public void add(Entity entity);
public void remove(Entity entity);
public void update();
// Decision, who needs access to these lists
public List<PaintableEntity> paintableEntities();
public List<MovableEntity> moveableEntities();
}
И поскольку нам действительно нужен какой-то способ работы ...
public class DefaultGameModel implements MutableGameModel {
private Set<Input> inputs = new HashSet<>();
private List<Entity> entities = new ArrayList<>(25);
@Override
public boolean hasInput(Input input) {
return inputs.contains(input);
}
@Override
public void setInput(Input input, boolean enabled) {
if (enabled) {
inputs.add(input);
} else {
inputs.remove(input);
}
}
@Override
public Dimension getViewableArea() {
return new Dimension(400, 400);
}
public void update() {
for (MovableEntity entity : moveableEntities()) {
entity.move(this);
}
}
// This is not the most efficent approach. You might consider
// caching each entity type into seperate lists when they are added
// instead
public List<PaintableEntity> paintableEntities() {
return entities.stream()
.filter(e -> e instanceof PaintableEntity)
.map(e -> (PaintableEntity) e)
.collect(Collectors.toList());
}
public List<MovableEntity> moveableEntities() {
return entities.stream()
.filter(e -> e instanceof MovableEntity)
.map(e -> (MovableEntity) e)
.collect(Collectors.toList());
}
@Override
public void add(Entity entity) {
entities.add(entity);
}
@Override
public void remove(Entity entity) {
entities.remove(entity);
}
}
Это довольно базовая концепция, но она закладывает основы остальной части работы - пожалуйста, не это "ПРИМЕР"Я сократил некоторые углы для краткости, чтобы я мог быстро его запустить и запустить, но основная концепция должна выдержать
И, наконец, мы добрались до «вида» (и контроллера).Это та часть, где мы следим за состояниями ввода, соответственно обновляем модель, запускаем основной цикл для обновления состояния модели, основываясь на текущих входах, расписании и выполнении рисования
public class GamePane extends JPanel {
private MutableGameModel model;
public GamePane(MutableGameModel model) {
this.model = model;
setupBindings();
Timer timer = new Timer(5, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
model.update();
repaint();
}
});
timer.start();
}
public void setupBindings() {
InputMap inputMap = getInputMap(WHEN_IN_FOCUSED_WINDOW);
ActionMap actionMap = getActionMap();
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, 0, false), "Up.pressed");
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, 0, true), "Up.released");
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_S, 0, false), "Down.pressed");
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_S, 0, true), "Down.released");
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_A, 0, false), "Left.pressed");
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_A, 0, true), "Left.released");
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_D, 0, false), "Right.pressed");
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_D, 0, true), "Right.released");
actionMap.put("Up.pressed", new InputAction(model, GameModel.Input.UP, true));
actionMap.put("Up.released", new InputAction(model, GameModel.Input.UP, false));
actionMap.put("Down.pressed", new InputAction(model, GameModel.Input.DOWN, true));
actionMap.put("Down.released", new InputAction(model, GameModel.Input.DOWN, false));
actionMap.put("Left.pressed", new InputAction(model, GameModel.Input.LEFT, true));
actionMap.put("Left.released", new InputAction(model, GameModel.Input.LEFT, false));
actionMap.put("Right.pressed", new InputAction(model, GameModel.Input.RIGHT, true));
actionMap.put("Right.released", new InputAction(model, GameModel.Input.RIGHT, false));
}
@Override
public Dimension getPreferredSize() {
return model.getViewableArea().getSize();
}
protected void paintComponent(Graphics g) {
super.paintComponent(g);
for (PaintableEntity entity : model.paintableEntities()) {
Graphics2D g2d = (Graphics2D) g.create();
entity.paint(g2d, model);
g2d.dispose();
}
}
}
public class InputAction extends AbstractAction {
private MutableGameModel model;
private GameModel.Input input;
private boolean pressed;
public InputAction(MutableGameModel model, GameModel.Input input, boolean pressed) {
this.model = model;
this.input = input;
this.pressed = pressed;
}
@Override
public void actionPerformed(ActionEvent e) {
model.setInput(input, pressed);
}
}
Довольно просто: P
В решении используются клавишные привязки , которые обычно менее беспокоят их KeyListener
и должны использоваться в 99% случаев, когда вы хотите отслеживать подмножество ввода.
Я также использовал Swing Timer
в качестве основного цикла.Это более безопасный вариант (в Swing), так как он не нарушает однопотоковую природу API
Пример ...
Потому что я знаю, как трудно это сделать и собрать вместе«фрагменты» кода ...
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
public class Test {
public static void main(String[] args) {
new Test();
}
public Test() {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
ex.printStackTrace();
}
DefaultGameModel model = new DefaultGameModel();
model.add(new BackgroundEntity());
model.add(new PlayerEntity());
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new GamePane(model));
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class GamePane extends JPanel {
private MutableGameModel model;
public GamePane(MutableGameModel model) {
this.model = model;
setupBindings();
Timer timer = new Timer(5, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
model.update();
repaint();
}
});
timer.start();
}
public void setupBindings() {
InputMap inputMap = getInputMap(WHEN_IN_FOCUSED_WINDOW);
ActionMap actionMap = getActionMap();
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, 0, false), "Up.pressed");
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, 0, true), "Up.released");
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_S, 0, false), "Down.pressed");
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_S, 0, true), "Down.released");
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_A, 0, false), "Left.pressed");
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_A, 0, true), "Left.released");
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_D, 0, false), "Right.pressed");
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_D, 0, true), "Right.released");
actionMap.put("Up.pressed", new InputAction(model, GameModel.Input.UP, true));
actionMap.put("Up.released", new InputAction(model, GameModel.Input.UP, false));
actionMap.put("Down.pressed", new InputAction(model, GameModel.Input.DOWN, true));
actionMap.put("Down.released", new InputAction(model, GameModel.Input.DOWN, false));
actionMap.put("Left.pressed", new InputAction(model, GameModel.Input.LEFT, true));
actionMap.put("Left.released", new InputAction(model, GameModel.Input.LEFT, false));
actionMap.put("Right.pressed", new InputAction(model, GameModel.Input.RIGHT, true));
actionMap.put("Right.released", new InputAction(model, GameModel.Input.RIGHT, false));
}
@Override
public Dimension getPreferredSize() {
return model.getViewableArea().getSize();
}
protected void paintComponent(Graphics g) {
super.paintComponent(g);
for (PaintableEntity entity : model.paintableEntities()) {
Graphics2D g2d = (Graphics2D) g.create();
entity.paint(g2d, model);
g2d.dispose();
}
}
}
public class InputAction extends AbstractAction {
private MutableGameModel model;
private GameModel.Input input;
private boolean pressed;
public InputAction(MutableGameModel model, GameModel.Input input, boolean pressed) {
this.model = model;
this.input = input;
this.pressed = pressed;
}
@Override
public void actionPerformed(ActionEvent e) {
model.setInput(input, pressed);
}
}
public class DefaultGameModel implements MutableGameModel {
private Set<Input> inputs = new HashSet<>();
private List<Entity> entities = new ArrayList<>(25);
@Override
public boolean hasInput(Input input) {
return inputs.contains(input);
}
@Override
public void setInput(Input input, boolean enabled) {
if (enabled) {
inputs.add(input);
} else {
inputs.remove(input);
}
}
@Override
public Dimension getViewableArea() {
return new Dimension(400, 400);
}
public void update() {
for (MovableEntity entity : moveableEntities()) {
entity.move(this);
}
}
// This is not the most efficent approach. You might consider
// caching each entity type into seperate lists when they are added
// instead
public List<PaintableEntity> paintableEntities() {
return entities.stream()
.filter(e -> e instanceof PaintableEntity)
.map(e -> (PaintableEntity) e)
.collect(Collectors.toList());
}
public List<MovableEntity> moveableEntities() {
return entities.stream()
.filter(e -> e instanceof MovableEntity)
.map(e -> (MovableEntity) e)
.collect(Collectors.toList());
}
@Override
public void add(Entity entity) {
entities.add(entity);
}
@Override
public void remove(Entity entity) {
entities.remove(entity);
}
}
public interface GameModel {
public enum Input {
UP, DOWN, LEFT, RIGHT;
}
public boolean hasInput(Input input);
public Dimension getViewableArea();
// Other properties which might be needed by the
// entities
}
public interface MutableGameModel extends GameModel {
public void setInput(Input input, boolean enabled);
public void add(Entity entity);
public void remove(Entity entity);
public void update();
// Decision, who needs access to these lists
public List<PaintableEntity> paintableEntities();
public List<MovableEntity> moveableEntities();
}
public interface Entity {
// Other common properties
}
public interface MovableEntity extends Entity {
public void move(GameModel model);
// Other "movable" entities might also have
// speed adjustments, but I might consider those as
// seperate entities in of themselves, but that's me
}
public interface PaintableEntity extends Entity {
public void paint(Graphics2D g2d, GameModel model);
}
public class BackgroundEntity implements PaintableEntity {
@Override
public void paint(Graphics2D g2d, GameModel model) {
g2d.setColor(Color.BLUE);
Dimension bounds = model.getViewableArea();
g2d.fillRect(0, 0, bounds.width, bounds.height);
}
}
public class PlayerEntity implements PaintableEntity, MovableEntity {
private int speed = 2;
private int x, y;
public PlayerEntity() {
// load the player image
}
public int getSpeed() {
return speed;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public void setX(int x) {
this.x = x;
}
public void setY(int y) {
this.y = y;
}
@Override
public void paint(Graphics2D g2d, GameModel model) {
g2d.setColor(Color.RED);
g2d.drawRect(getX(), getY(), 10, 10);
}
@Override
public void move(GameModel model) {
int speed = getSpeed();
int x = getX();
int y = getY();
if (model.hasInput(GameModel.Input.UP)) {
y -= speed;
} else if (model.hasInput(GameModel.Input.DOWN)) {
y += speed;
}
if (model.hasInput(GameModel.Input.LEFT)) {
x -= speed;
} else if (model.hasInput(GameModel.Input.RIGHT)) {
x += speed;
}
Dimension bounds = model.getViewableArea();
if (y < 0) {
y = 0;
} else if (y + 10 > bounds.height) {
y = bounds.height - 10;
}
if (x < 0) {
x = 0;
} else if (x + 10 > bounds.width) {
x = bounds.width - 10;
}
setX(x);
setY(y);
}
}
}
nb - Для каждого, кто расскажет больше, насколько ужасно неэффективно использовать List#stream
таким образом, да, выправильно, я уже упоминал, что несколько раз - смысл в краткости.
Если бы я делал это, я бы либо ограничил требования на MutableModelLevel
(используя такие вещи, как add(PaintableEntity)
, либо попросил бы метод add определить, какую серию List
s нужно добавить к сущностиили) создать серию общих «моделей», которые ограничивают функциональность, так что, когда я разрабатываю свою реализацию, я могу выбирать, какую функциональность я хочу использовать - но это только я