주제 선정
Java를 어느 정도 배우고 약 4일간의 미니 프로젝트를 진행하였고 저희 조는 비행기 게임을 만들기로 하였습니다.
탑건에서 이름을 따서 자바건 -> 잡건 -> 짭건 으로 프로젝트 이름이 결정되었습니다. ㅎㅎ
비행기, 적비행기 등을 설계하며 객체지향을 잘 적용해 볼 수 있을 것 같았습니다.
소스 코드
https://github.com/PDA-JJAPGUN/JJAPGUN
GitHub - PDA-JJAPGUN/JJAPGUN: 팀프로젝트 JJAPGUN: 오락실 비행기 게임
팀프로젝트 JJAPGUN: 오락실 비행기 게임. Contribute to PDA-JJAPGUN/JJAPGUN development by creating an account on GitHub.
github.com
시연 영상
https://www.youtube.com/watch?v=Wa8OuhzEfSo
맡은 역할
PL(Project Leader)로 PM과 함께 프로젝트를 주도하였습니다.
주로 제가 한 일은 아래와 같습니다.
- Daily TO DO 정리
- TO DO 기반 업무 배분
- 각 팀원의 일정과 원하는 일을 맡을 수 있도록 고려하였습니다.
실제 프로젝트 시 작성했던 노션
- 각 팀원의 일정과 원하는 일을 맡을 수 있도록 고려하였습니다.
미리 TO DO를 만들어 놓으니 회고를 작성할 때 성과, 남은 일, 내일 할 일 등을 작성하기 용이하였고
팀원들도 미리 TO DO를 보고 해보고 싶은 일이나 잘할 수 있는 일을 미리 알려주어 업무를 배분하기 쉬웠습니다.
- UI/UX 설계
피그마를 통해 와이어프레임 수준의 간단한 UI/UX를 설계하였습니다.
개발적으로 맡은 기능은 아래와 같습니다.
- Panel 개발 및 전체 Panel 연결
- 랭킹 기능
- 로그아웃 기능
- 게임 컨트롤러 제작
구현 기능
Panel 개발 및 전체 Panel 연결
Java의 Swing을 사용하여 GameFrame 내의 Panel들을 구현하였습니다.
- Class GameTitle
GameTitle의 베이스 클래스를 만든 후에 이를 상속하여
게임 시작 Panel (GameStart), 게임 오버 Panel (GameEnd), 게임 랭크 Panel(GameRank)의 각 화면을 만들었습니다.
상속을 함으로써
동일한 배경을 사용하는 Panel들이기에 상속을 하여 여러번 배경을 설정하지 않아도 되는 이점이 있었습니다.
자주 사용하는 JLabel 과 JButton을 쉽게 만들어주는 메서드를 선언하여 자식 Panel들이 쉽게 사용할 수 있게 되었습니다.
각 버튼의 액션에 따른 이벤트를 관리할 수 있었습니다.
package view;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class GameTitle extends JPanel implements ScreenSize, ActionListener {
private ImageIcon backgroundIcon = new ImageIcon("images/GameTitle.png");
private Image backgroundImage = backgroundIcon.getImage();
private GameFrame gameFrame;
public GameTitle(GameFrame gameFrame) {
this.gameFrame = gameFrame;
}
protected JButton createButton(String text, int width, int height, int x, int y) {
JButton button = new JButton(text);
button.setBounds(x, y, width, height);
button.setActionCommand(text); // 버튼 이름과 동일하게 처리, 버튼 추가 시 ActionCommand 에 추가
button.addActionListener(this);
return button;
}
protected JLabel createLabel(String text, Color backgroundColor, Color textColor) {
JLabel jLabel = new JLabel(text);
if (backgroundColor != null) {
jLabel.setOpaque(true); // 불투명도를 참으로 설정하여 배경색을 보이게 한다
jLabel.setBackground(backgroundColor);
}
jLabel.setForeground(textColor);
return jLabel;
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
g.drawImage(backgroundImage, 0, 0, SCREEN_WIDTH - 15, SCREEN_HEIGHT, 0, 0, 338, 594, this);
}
@Override
public void actionPerformed(ActionEvent e) {
// 버튼을 눌렀을 때
switch (ActionCommand.valueOf(e.getActionCommand())) {
case START:
gameFrame.change(Panel.GAME_SELECT_PLAYER.name());
break;
case RANK:
gameFrame.change(Panel.GAME_RANK.name());
break;
case X: // 랭킹 화면에서 닫기 버튼 눌렀을 때
gameFrame.change(Panel.GAME_END.name());
break;
case EXIT:
gameFrame.dispose();
break;
default:
break;
}
}
int setX(int width) {
return (SCREEN_WIDTH - width) / 2;
}
int setY(int height) {
return SCREEN_HEIGHT - height - 50;
}
}
- 게임 시작 Panel (Class GameStart)
UserController에서 가져온 현재 로그인 된 User의 닉네임과 함께 환영 문구를 표시합니다.
JLabel welcomeMsg = createLabel("Welcome, " + UserController.getInstance().getLogginedUser().getNickname() + "!", null, Color.WHITE);
add(welcomeMsg);
START 버튼을 누르면 GameTitle의 createButton 메서드를 통해 버튼 Text에 따라 액션이 등록됩니다.
플레이어 선택 Panel로 변경합니다.
/* GameStart */
JButton startBtn = createButton(ActionCommand.START.name(), width, height, (SCREEN_WIDTH - width) / 2, SCREEN_HEIGHT - height - 50);
add(startBtn);
/* GameTitle */
@Override
public void actionPerformed(ActionEvent e) {
// 버튼을 눌렀을 때
switch (ActionCommand.valueOf(e.getActionCommand())) {
case START:
gameFrame.change(Panel.GAME_SELECT_PLAYER.name());
break;
...
}
각 버튼 Text(즉, ActionCommand)는 enum을 통해 관리하여 오타와 같은 실수를 방지하고자 하였습니다.
package view;
public enum ActionCommand {
START, RANK, EXIT, X;
}
- 게임 오버 Panel(Class GameEnd)
RANK 버튼과 EXIT 버튼도 마찬가지로 GameTitle의 createButton 버튼 메소드로 만들었습니다.
RANK 버튼을 누르면 게임 랭크 패널이 켜지고, EXIT 버튼을 누르면 GameFrame이 dispose 됩니다. (창이 꺼집니다.)
/* GameEnd */
JButton rankBtn = createButton(ActionCommand.RANK.name(), width, height, setX(width) - width, setY(height));
add(rankBtn);
JButton exitBtn = createButton(ActionCommand.EXIT.name(), width, height, setX(width) + width, setY(height));
add(exitBtn);
/* GameTitle */
@Override
public void actionPerformed(ActionEvent e) {
// 버튼을 눌렀을 때
switch (ActionCommand.valueOf(e.getActionCommand())) {
...
case RANK:
gameFrame.change(Panel.GAME_RANK.name());
break;
case X: // 랭킹 화면에서 닫기 버튼 눌렀을 때
gameFrame.change(Panel.GAME_END.name());
break;
...
}
}
GameController를 통해 게임 결과와 현재 점수를 가져옵니다.
JLabel gameResult = createLabel(gameController.isGameWin ? "YOU WIN" : "YOU LOSE", null, Color.WHITE);
add(gameResult);
JLabel currScore = createLabel(String.format("SCORE: %d", gameController.getFinalScore()), Color.WHITE, Color.BLACK);
add(currScore);
UserController를 통해 로그인 한 User의 최고 점수를 가져옵니다.
UserEntity user = UserController.getInstance().getLogginedUser();
JLabel bestScore = createLabel(String.format("BEST SCORE: %d", user.getBestScore()), Color.WHITE, Color.BLACK);
bestScore.setText("BEST SCORE: " + user.getBestScore());
add(bestScore);
- 게임 랭크 Panel (GameRank)
UserController를 통해 UserService의 getRanks 메서드를 실행하여
UserDAO에 저장되었고 bestScore가 null이 아닌 것을 내림차순으로 정렬합니다.
/* UserController */
public List<UserEntity> getRanks() {
return userService.getRanks();
}
/* UserService */
public List<UserEntity> getRanks() {
List<UserEntity> userEntities = userDao.getUsers();
return userEntities.stream()
.filter(user -> user.getBestScore() != null)
.sorted(Comparator.comparing(UserEntity::getBestScore).reversed())
.collect(Collectors.toList());
}
가져온 Rank 정보를 반복하며 순위, 이름, 점수를 표시합니다.
package view;
import controller.GameController;
import controller.UserController;
import entity.UserEntity;
import javax.swing.*;
import java.awt.*;
import java.util.List;
public class GameRank extends GameTitle {
private ImageIcon LeaderBoardIcon = new ImageIcon("images/LeaderBoard.png");
private Image LeaderBoardImg = LeaderBoardIcon.getImage();
public GameRank(GameFrame gameFrame) {
super(gameFrame);
setLayout(null);
// 배경 패널을 추가
BackgroundPanel backgroundPanel = new BackgroundPanel(LeaderBoardImg);
backgroundPanel.setBounds(40, 25, SCREEN_WIDTH - 100, SCREEN_HEIGHT - 100);
add(backgroundPanel);
backgroundPanel.setLayout(null);
JButton closeBtn = createButton("X", 50, 50, SCREEN_WIDTH - 150, 0);
backgroundPanel.add(closeBtn);
List<UserEntity> users = UserController.getInstance().getRanks();
// 사용자 순위 정보 추가
int rankNumber = 0;
for (UserEntity user : users) {
if (rankNumber < 12) {
int y = 150 + rankNumber * 45;
JLabel rank = new JLabel(Integer.toString(++rankNumber));
JLabel nickname = new JLabel(user.getNickname());
JLabel score = new JLabel(Integer.toString(user.getBestScore()));
...
backgroundPanel.add(rank);
backgroundPanel.add(nickname);
backgroundPanel.add(score);
}
}
}
}
- GameFrame
Panel 변경 메서드입니다. panelName을 기반으로 GameFrame 내 Panel을 교체합니다.
public void change(String panelName) {
getContentPane().removeAll();
switch (Panel.valueOf(panelName)) {
case GAME_START:
gameStart = new GameStart(gameFrame);
getContentPane().add(gameStart);
break;
case GAME_MAP:
gameMap = new GameMap(gameFrame);
getContentPane().add(gameMap);
break;
case GAME_END:
gameEnd = new GameEnd(gameFrame);
getContentPane().add(gameEnd);
break;
case GAME_RANK:
gameRank = new GameRank(gameFrame);
getContentPane().add(gameRank);
break;
case GAME_SELECT_PLAYER:
selectPlayer = new SelectPlayer(gameFrame);
getContentPane().add(selectPlayer);
break;
}
revalidate();
repaint();
}
PanelName 또한 enum으로 관리하여 하드 코딩을 방지하고자 하였습니다.
package view;
public enum Panel {
GAME_START,
GAME_SELECT_PLAYER,
GAME_MAP,
GAME_END,
GAME_RANK,
}
게임 컨트롤러 제작
게임 시작 메서드는 GameStart class에서 호출하고
게임 오버 메서드는 Player class에서 Life가 0이 될 시, PlayerAttack class에서 Boss의 Hp가 0이 될 시 호출하고
게임 점수 필드는 Player class, GameRank class에서 필요한 상황이었습니다.
때문에 이를 관리하는 게임 컨트롤러가 있어야 겠다고 판단하여, 여러 클래스에서 쉽게 접근할 수 있도록 싱글톤 패턴으로 제작하였습니다.
package controller;
import dao.impl.UserDAOImpl;
import entity.UserEntity;
import service.UserService;
import view.GameFrame;
import view.Panel;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
public class GameController {
private static final GameController instance = new GameController();
// private 생성자를 통해 외부에서 인스턴스 생성을 방지
public GameController() {}
// 싱글톤 인스턴스를 반환하는 메소드
public static GameController getInstance() {
return instance;
}
GameFrame gameFrame;
public boolean isGameWin = false;
public boolean getIsGameWin() {
return isGameWin;
}
public void gameStart() {
new GameFrame();
}
public void setGameWin(boolean gameWin) {
isGameWin = gameWin;
}
public void gameEnd() {
gameFrame.change(Panel.GAME_END.name());
}
public void setGameFrame(GameFrame gameFrame) {
this.gameFrame = gameFrame;
}
public int getFinalScore() {
return gameFrame.player.score;
}
public void gameOver(boolean isGameWin) {
setGameWin(isGameWin);
if (gameFrame != null) {
gameFrame.change(Panel.GAME_END.name());
} else {
System.out.println(gameFrame);
}
}
}
로그아웃
- 로그인 시 로그아웃으로 버튼 내 글자를 변경하였습니다.
- 로그아웃 버튼 클릭 시 메서드 logout이 실행됩니다.
- 로그아웃 성공 메세지
- 아이디, 비밀번호, 닉네임 입력값 초기화
- 로그아웃 버튼 내 글자 로그인으로 변경
@Override
public void actionPerformed(ActionEvent e) {
...
else if (e.getSource() == btn_logout) {
logout();
}
...
}
void logout() {
UserSession.getInstance().logout();
btn_logout.setVisible(false);
btn_login.setVisible(true);
la_result.setText("로그아웃 성공");
la_result.setForeground(Color.BLUE);
tf_nickname.setText("");
tf_id.setText("");
tf_password.setText("");
isLogin = false;
}
아쉬웠던 점
- 일정 관리
프로젝트 초기에 기획에 많은 시간을 쏟아 설계 및 개발에 투자할 시간이 절대적으로 부족했습니다.
또한 초반에는 Task가 팀원 전반적으로 공유되지 않아서 혼란이 있었습니다.
이를 해결하기 위해 TO DO 를 전체적으로 파악한 뒤 팀원들에게 공유하였습니다.
팀원이 원하는 기능별로 업무를 배분하고 해치우는 식으로 하다 보니 비교적 빠르게 개발을 진행할 수 있었습니다.
- 급한 코딩, 꼬이는 코드 .. MVC..
앞서 말했듯 절대적으로 시간이 부족하여, 우선 기능 완성에 집중하기 위해 노력하였습니다.
때문에 중복되는 코드나 의존성이 강한 코드가 매우 많습니다.
우선 완성을 한 뒤 테스트를 거치면서 발표 당일까지 눈에 보이는 대로 리팩터링을 진행하였습니다.
또한 아직 MVC 구조를 게임에서 제대로 적용하지 못한 점이 아쉬웠습니다.
좋았던 점
- Java로 게임을 만들어보다니!!
게임은 Unity나 Unreal 등의 엔진을 이용해서만 만들어봤었는데,
순수 Java로만 게임을 구현해보는 신기한 경험을 할 수 있었습니다.
Java로 모바일 프로그래밍 제외하고는 첫 프로젝트였는데
Swing, Thread 등 본래 알던 Java보다 더 많은 것을 다뤄볼 수 있어서 어려웠지만 흥미로웠습니다.
- 배운 것을 실제로 적용해보며 와닿는 계기
약 일주일 ~ 10일? 간 배웠던 객체지향과 클래스, 인터페이스 등을 최대한 적용해보려고 노력했습니다.
비행기의 공통 기능을 인터페이스로 규약을 내리고
화면 설계 같은 경우 공통된 부분을 부모 클래스에서, 세부적으로 나뉘는 부분을 상속을 통해 자식별로 다르게 구현해보았습니다.
이론만 해보다가 실제로 적용해보고 고민해보며 추상적일 수 있는 개념들이 더 와닿게 되는 계기였습니다.
MVC 패턴도 좀 더 고민해보면서 어떻게 리팩토링할 수 있을지, 고민해봐야겠습니다.
- 기능 구현 후 수정은 나중에를 실천
이전 React 프로젝트 때 구현하면서 수정해야할 게 계속 생겨서 이를 고치다보니 뒤의 기능에 힘을 못써 완성도가 떨어졌었습니다.
강사님께서 목표한 기능을 구현 후, 추가적인 수정 사항은 나중에 발전시키라는 조언을 하셨습니다.
이번 프로젝트는 이를 실천할 수 있었던 기회였습니다.
비행기 게임 + 랭킹 의 최소 기능 목표를 설정하고
우선 완성시킨 뒤, 추가적으로 보이는 디테일들을 개선해 나가는 방식으로 프로젝트를 이끌었습니다.
훨씬 시간도 단축되었고 완성도를 높일 수 있었습니다.
- PL으로 주도하는 경험
프로젝트 초반에 비교적 진행 속도가 느려서 시간을 허비했었습니다.
남은 시간을 최대한 활용하기 위해, TO DO를 통해 구현해야 할 기능들을 리스트업 한 후,
큰 기능별로 2명씩 페어 프로그래밍을 통해 서로 설계나 고민을 같이 해보며 협업할 수 있도록 하였습니다.
지속적으로 팀원이 어떤 일을 하고 있는지, 진행도는 어떤지 체크하였습니다.
전체적인 진행 상황을 알고 있으니 어디까지 진행되고 있는지 알 수 있었고,
어떤 이슈가 발생했다면 담당자를 연결해주어 원활한 해결이 가능하게 할 수 있었습니다.
혼자 개발했다면 완성하기 힘들 수 있었을 것이지만,
업무 배분을 적절히 한 협업을 통해 프로젝트를 완성까지 무사히 이끌 수 있었습니다.
하얗게 불태웠다 .. 🔥