I'm trying to learn how to program with Java which is more or less my first programming language (had some basic Pascal and C,C++ in school and college) but still a Beginner, with a capital 'B'.
Right now I'm working on a mini game, which is similar to the "breakout" game, but it's going to have my own implementation and won't have any blocks to destroy (I know it sounds weird). I have written some core classes, created a field, a pad and a ball. I'd like to get some feedback on my coding: how could I improve my code so it would be easier to implement new features and I'd like to know some of my bad habits that I should try to avoid doing to become good in writing code?
Here are my classes that I have already written:
Game class:
package game;
import java.awt.EventQueue;
import javax.swing.JFrame;
public class Game extends JFrame {
public Game(){
Field field = new Field();
setContentPane(field);
pack();
setDefaultCloseOperation(EXIT_ON_CLOSE);
setTitle("BLOCK GAME bETA");
setVisible(true);
}
public static void main(String[] args) {
EventQueue.invokeLater(() -> {
Game ex = new Game();
ex.setVisible(true);
});
}
}
Field class:
package game;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.Timer;
import java.util.TimerTask;
import javax.swing.JLabel;
public class Field extends JLabel{
//Dimensions of the field.
public final int FIELD_HEIGHT = 800;
public final int FIELD_WIDTH = 480;
//Creates a pad.
static Pad pad = new Pad();
//Creates a ball on top of the pad.
static Ball ball = new Ball(pad.getPadX(), pad.getPadY());
private Timer timer;
//Field constructor, creates a game field.
public Field(){
setPreferredSize(new Dimension(FIELD_WIDTH, FIELD_HEIGHT));
setFocusable(true);
addKeyListener(new Control());
timer = new Timer();
timer.scheduleAtFixedRate(new ScheduleTask(), 100, 20);
}
//Draws all of the components on the screen.
@Override
public void paintComponent(Graphics g){
super.paintComponent(g);
setOpaque(true);
setBackground(new Color(27, 89, 195));
drawFieldLimits(g);
drawPad(g);
drawBall(g);
}
private void drawFieldLimits(Graphics g){
// 15 pixel offset from the boarder of the frame.
g.drawLine(15, 15, 15, FIELD_HEIGHT - 15);
g.drawLine(15, 15, FIELD_WIDTH - 15, 15);
g.drawLine(FIELD_WIDTH - 15, 15, FIELD_WIDTH - 15, FIELD_HEIGHT - 15);
}
private void drawPad(Graphics g){
g.fillRect(pad.getPadX(), pad.getPadY(), pad.getPadWidth(), pad.getPadHeight());
}
private void drawBall(Graphics g){
g.fillOval(ball.getBallX(), ball.getBallY(), ball.getBallDiamiter(), ball.getBallDiamiter());
}
private class Control extends KeyAdapter{
@Override
public void keyReleased(KeyEvent e){
pad.keyReleased(e);
}
@Override
public void keyPressed(KeyEvent e){
pad.keyPressed(e);
}
}
private class ScheduleTask extends TimerTask{
@Override
public void run() {
pad.movement();
ball.movement(pad.getPadX(), pad.getPadY());
repaint();
}
}
}
Pad class:
package game;
import java.awt.event.KeyEvent;
public class Pad extends Field{
private final int PAD_WIDTH = 60;
private final int PAD_HEIGHT = 10;
//Starting point of the pad.
private final int INITIAL_X = (FIELD_WIDTH/2) - (PAD_WIDTH/2);
private final int INITIAL_Y = FIELD_HEIGHT - PAD_HEIGHT - 10;
//Starting speed of the pad, may change during the game.
private int initialPadSpeed = 5;
private int padSpeed;
//Pad position.
private int padX, padY;
//Constructor
public Pad(){
padX = INITIAL_X;
padY = INITIAL_Y;
}
//Defines the movement of the pad.
public void movement(){
padX += padSpeed;
if(padX <= 15){
padX = 15;
}
// 15 is the offset.
if(padX >= FIELD_WIDTH - PAD_WIDTH - 15){
padX = FIELD_WIDTH - PAD_WIDTH - 15;
}
}
//Getters.
public int getPadX(){
return padX;
}
public int getPadY(){
return padY;
}
public int getPadWidth(){
return PAD_WIDTH;
}
public int getPadHeight(){
return PAD_HEIGHT;
}
//Action events used to move the pad left and right.
public void keyPressed(KeyEvent e) {
if(e.getKeyCode() == KeyEvent.VK_LEFT){
padSpeed = -initialPadSpeed;
}
if(e.getKeyCode() == KeyEvent.VK_RIGHT){
padSpeed = initialPadSpeed;
}
}
public void keyReleased(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_LEFT) {
padSpeed = 0;
}
if (e.getKeyCode() == KeyEvent.VK_RIGHT) {
padSpeed = 0;
}
}
}
Ball class:
package game;
public class Ball extends Pad{
//Variables that define the ball.
private int ballX, ballY;
private int initialBallSpeedVertical = 2;
private int initialBallSpeedHorizontal = 2;
private int ballSpeedVertical, ballSpeedHorizontal;
private int ballDiameter = 10;
//Constructor that has pad position coordinates as parameters, so the ball will be created on the pad.
public Ball(int x, int y) {
ballX = x + 30; // + pad width so the ball is at the middle of the pad.
ballY = y;
ballSpeedVertical = initialBallSpeedVertical;
ballSpeedHorizontal = initialBallSpeedHorizontal;
}
//Defines how the ball moves during the game. Consists pad position coordinates
//so the ball could bounce of the pad.
public void movement(int padX, int padY){
ballX -= ballSpeedHorizontal;
ballY -= ballSpeedVertical;
if(ballX <= 15){
ballSpeedHorizontal =-ballSpeedHorizontal;
}
if(ballX >= FIELD_WIDTH - ballDiameter - 15){
ballSpeedHorizontal = ballSpeedHorizontal =-ballSpeedHorizontal;
}
if(ballY <= 15){
ballSpeedVertical = -ballSpeedVertical;
}
if(ballY >= FIELD_HEIGHT - ballDiameter){
ballSpeedHorizontal = 0;
ballSpeedVertical = 0;
}
//Bouncing of the pad.
if((ballX >= getPadX() && (ballX <= padX + getPadWidth()) && (ballY >= padY))){
ballSpeedVertical = -ballSpeedVertical;
}
}
//Getters.
public int getBallX(){
return ballX;
}
public int getBallY(){
return ballY;
}
public int getBallDiamiter(){
return ballDiameter;
}
}
I'd also like to add that I have written this using numerous of online tutorials as an examples and help. I'd also like to know why I get StackOverflowError
if I don't assign pad and ball objects as static in Field class because it worked for people who wrote tutorials that I was using?
Also, I know that I could have made some variables final
but I think they might be changeable on the future versions of the game (e.g. the ones that determine pad and ball speed).
Also, feel free to bash me for my beginner skills (or lack of them) and don't be afraid of giving me some harsh criticism. Any feedback with coding advice will be much appreciated.
-
\$\begingroup\$ Please put notes at the top, so people see them when they first open the question. Unless it is a footnote, of course. \$\endgroup\$FreezePhoenix– FreezePhoenix2018年08月09日 19:22:02 +00:00Commented Aug 9, 2018 at 19:22
-
\$\begingroup\$ Welcome to Code Review! The original title, which stated your concerns about the code, applies to too many questions on this site to be useful. The site standard is for the title to simply state the task accomplished by the code. I've edited it to help you; please check that I haven't misrepresented your code! \$\endgroup\$Toby Speight– Toby Speight2018年08月09日 19:38:39 +00:00Commented Aug 9, 2018 at 19:38
-
\$\begingroup\$ Why are ball and pad static? Avoid static wherever possible. Static members are really hard to test, especially in larger applications (okay, not the case in your game, just a general advice) \$\endgroup\$Mirco– Mirco2018年08月09日 19:49:07 +00:00Commented Aug 9, 2018 at 19:49
1 Answer 1
First, the small stuff.
getBallDiamiter
has a misspelling.movement
kinda sucks as a method name.move
would be better, but makesball.move(30, 100)
a bit ambiguous.It's better if classes that don't have to be public, aren't. Here, the only thing that needs to be public is
Game
, as it's your entry point.If you have to ask another object for all the info you need to do your job, it shouldn't be your job.
To that end, the
drawPad
anddrawBall
methods inField
shouldn't be there. They should be inPad
andBall
, respectively.Once you move them, there's nothing outside of the
Ball
class that needs to know the ball's position or diameter, and therefore no reason for any of those getters to exist.While we're messing around with
paintComponent
, a more appropriate place to set opacity, background, etc is in the constructor. It won't change unless something else changes it.The magic number 15 is everywhere. Looks like it's being used as the game area's margin. Put that number in a constant or something.
You don't seem to use any functionality specific to
JLabel
. Pick a more appropriate base class.JPanel
works, as by definition it's a generic component.Components have a
processComponentKeyEvent
method, so you don't need to attach a separate key event listener. (On the minus side, the same function processes key-pressed and key-released events as well as key-typed events, so you have to distinguish between them.) Or, if you wanted, you could havePad
implementKeyListener
, and add an emptykeyReleased
method, then the helper class again goes away.If you use a
javax.swing.Timer
orjava.util.concurrent.ScheduledExecutorService
rather than ajava.util.Timer
, you get the ability to use a method reference. So you don't need to write a class just to respond to timer ticks.Your field and method names shouldn't repeat the class name.
If you're doing things right, your field and method names don't need to mention the class name. If i'm looking in the
Ball
class, a field namedx
should be the ball's X coordinate, and if you insist on a getter for it, it should be namedgetX
.
With that, we get into the elephant in the room here. Yeah, i know why you have getPadX
, getBallX
, etc, rather than just calling them getX
. And that's because
You're abusing the hell out of inheritance. :P
Question: Is a
Ball
a special kind ofPad
? Is aPad
a special kind ofField
?Answer: No, and no.
Pad
andBall
, you don't even treat as Swing components, let alone labels, let alone game UI areas...and you never treat aBall
like aPad
. If you were to even try, it would look very strange.I see why you're inheriting; you want access to
FIELD_WIDTH
,getPadWidth()
, etc. But there are cleaner ways to get access to that info. Ideally, there are ways to remove the need to even know it, and/or move that need to somewhere that can fulfill it better.For now, though, the simplest change that would work would be to pass the field to
Pad
's constructor, and pass the field and pad both toBall()
. It's an ugly band-aid, but eliminates inheritance forPad
andBall
altogether, and opens up a number of other possibilities.
With that fixed, a couple of other things become fixable:
Static mutable state is the devil. And you only have to do it at all because inheritance breaks otherwise. Once you have everything holding proper references to its dependencies, you can make
pad
andball
private instance fields.You've hard-coded a bunch of things that don't make sense being hard-coded. Again, much of that was entirely to keep inheritance working. But the field, being a Swing component, provides a way to get its actual size...and the pad already provides its own API to do so. While it wasn't as feasible back when everything was shackled together by inheritance, it's downright trivial now. A couple of helper methods, like
getLeft()
andgetRight()
, can even take margins into account so that the ball needn't know that margins even exist.
-
\$\begingroup\$ Wow! Thank you for taking your time to answer. Since I'm learning, It's my first time trying to use inheritance and I went with the first thing that worked, which was a mistake and using inheritance properly should fix a huge chunk of my problems. Thank you a lot. \$\endgroup\$user176827– user1768272018年08月11日 16:04:33 +00:00Commented Aug 11, 2018 at 16:04