코딩캠프 미니프로젝트 마무리

건희·2023년 1월 14일
1

코딩캠프 (탱크 게임 , 테트리스 실행결과)

2023.01.09 ~ 2023.01.12 코딩캠프

  • 탱크 게임

탱크 게임

  • CannonMain.java
  • CannonFrame.java
  • CannonPanel.java
  • GamePanel.java
  • CannonThread.java
  • TargetThread.java

총 6개의 java 파일로 이루어진 미니 프로젝트


  • CannonMain.java : CannonFrame 객체를 생성하여 게임을 실행시켜주는 Main 메소드가 작성되어있는 파일
  • CannonFrame.java : CannonFrame Class 를 구현해놓은 java 파일. CannonPanel을 ContentPane으로 설정한다
  • CannonPanel.java : 게임 진행 시의 포탄수를 조정하고 , 발사 버튼을 누르면 CannonThread가 실행되어 게임 진행이 가능하게 해주는 Panel
  • GamePanel.java : 게임이 실제로 진행되는 패널. CannonThread 스레드가 생성되고 실행되면 GamePanel에서 제공하는 메소드를 통해 GamePanel에 있는 여러 객체들을 제어 , KeyAdapter 이벤트리스너를 통해 키로도 객체들을 제어하며 게임이 진행된다.
  • CannonThread.java : 스레드의 메소드에는 총알이 움직이는 모습을 보여주는 moveBullet() 메소드가 있고 , wait-notify 기능과 Interrupt 를 할 수 있도록 해주는 메소드 , 그리고 스레드가 실행될 수 있도록 해주는 run() 메소드가 있다.
  • TargetThread.java : 게임에서 움직이는 Target의 움직임을 구현하는 스레드.
public class CannonMain {
	public static void main(String[] args) {
		new CannonFrame();  //게임 실행
	}
}
import java.awt.Color;
import javax.swing.JFrame;

public class CannonFrame extends JFrame{
	private CannonPanel cp=new CannonPanel(); //ContentPane으로 설정할 CannonPanel 선언
	public CannonFrame() {
		super("Tank Game"); //Frame 의 제목
		setSize(800,500); //Frame의 크기를 정해준다
		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // 게임 Frame을 닫을 시 mainThread도 종료된다
		setLocation(300,200); //게임 실행 시 Frame의 위치 설정
		setResizable(false); //크기를 조정 불가능하게 한다
		setContentPane(cp); // CannonPanel을 ContentPane으로 설정
		setVisible(true); // 현재 Frame과 부착된 컴포넌트들이 보이게
	}
}
import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;

public class CannonPanel extends JPanel{
	private int width; //Panel의 width , 바로 알기 위해서는 ComponentListener 이벤트리스너를 통해 얻어와야한다
	private int height; //Panel의 height , 바로 알기 위해서는 ComponentListener 이벤트리스너를 통해 얻어와야한다
	private int count; //대포알의 갯수
	private GamePanel gamePanel=new GamePanel(this,100,300); //게임패널을 이 패널의 중앙에 붙이기
	
	private JLabel countLabel=new JLabel("포탄수");
	private JTextField textField1=new JTextField(); //텍스트필드
	private JButton shootButton=new JButton("발사"); //발사 버튼
	public CannonPanel() {
		setLayout(null); //배치관리자 null로 설정
		setBackground(new Color(146,146,146));
		//버튼, 텍스트 필드 등 배치.
		JButton startbtn=new JButton("발사"); //포 발사 버튼 , 누를 때마다 대포알 벡터에 추가하며 스레드 실행.
		this.addComponentListener(new ComponentAdapter() {
			@Override
			public void componentResized(ComponentEvent e) {
				width=getWidth();
				height=getHeight();
				
				textField1.setBackground(new Color(180,180,180));
				textField1.setSize(50,20);
				textField1.setLocation(width/2-25,height-80);
				countLabel.setSize(50,20);
				countLabel.setLocation(width/2-70,height-80);
				shootButton.setSize(80,40);
				shootButton.setLocation(width/2-55,height-50);
		
				
				shootButton.addActionListener(new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent e) {
						
						gamePanel.setBeforeCount();
						count=Integer.parseInt(textField1.getText());
						gamePanel.setScore(0);
						gamePanel.setScoreLabel();
						//인자로 갯수 넘겨주어야 함 (포탄 수 , 사거리 , 각도)
						gamePanel.startCannonThread(count); //버튼 누르면 bulletThread 실행
						gamePanel.requestFocus();
					}
				});
				add(textField1);
				add(countLabel);
				add(shootButton);
			}
		});
		gamePanel.setSize(750,350);
		gamePanel.setLocation(18,15);
		this.add(gamePanel);
	}
}
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.Vector;

import javax.swing.JLabel;
import javax.swing.JPanel;

public class GamePanel extends JPanel{ //실제 게임이 진행되는 패널
	private int width;
	private int height;
	private int x; //포탄의 시작 x좌표
	private int y; //포탄의 시작 y좌표
	private int score=0; //포탄 맞은 갯수
	private Tank tank=new Tank(100,100); //탱크의 좌표를 주어주며 객체 생성
	private Vector<Bullet> bulletVector=new Vector<Bullet>(); //대포알 수만큼 벡터에 저장
	private Target target; //타겟의 시작 , 끝 지점 정보를 가지고있어야함
	private CannonThread cannonThread=null;
	private int bulletSize; //대포알 수 지정되는 int 형 필드
	private CannonPanel cannonPanel; // CannonPanel에서 GamePanel 메소드를 사용
	private JLabel scoreLabel=new JLabel("0");
	private JLabel beforeScoreLabel=new JLabel("0");
	private JLabel nowLabel=new JLabel("이번 게임");
	private JLabel currentLabel=new JLabel("이전 게임");
	private TargetThread targetThread; //타겟 스레드 레퍼런스 선어
	
	public GamePanel(CannonPanel cannonPanel,int x,int y) {
		this.setLayout(null);
		this.cannonPanel=cannonPanel;
		this.x=x;
		this.y=y;
		this.tank=new Tank(x,y); //위치 조정은 인자로 받아오게 하자
		setBackground(Color.WHITE);
		//this.bullet=new Bullet(x+10,y,10); //얘도 위치조정은 인자로
		this.addComponentListener(new ComponentAdapter() {
			@Override
			public void componentResized(ComponentEvent e) {
				width=getWidth();
				height=getHeight();
				
				GamePanel.this.setFocusable(true);
				scoreLabel.setFont(new Font("고딕",Font.ITALIC,40));
				scoreLabel.setSize(100,50);
				scoreLabel.setLocation(width/2-25,40);
				beforeScoreLabel.setFont(new Font("고딕",Font.ITALIC,40));
				beforeScoreLabel.setSize(100,50);
				beforeScoreLabel.setLocation(width/2-25,0);
				nowLabel.setFont(new Font("고딕",Font.BOLD,30));
				nowLabel.setSize(200,50);
				nowLabel.setLocation(width/2-180,40);
				currentLabel.setFont(new Font("고딕",Font.BOLD,30));
				currentLabel.setSize(200,50);
				currentLabel.setLocation(width/2-180,0);
			}
		});
		this.addKeyListener(new KeyAdapter() {
			@Override
			public void keyPressed(KeyEvent e){
				int keyCode=e.getKeyCode();
				if(keyCode==40 && tank.getY()<310) { //arrow down
					tank.setY(tank.getY()+10);
				}
				else if(keyCode==38 && tank.getY()>10) { //arrow up
					tank.setY(tank.getY()-10);
				}
			}
		});
		
		GamePanel.this.add(scoreLabel);
		GamePanel.this.add(beforeScoreLabel);
		GamePanel.this.add(nowLabel);
		GamePanel.this.add(currentLabel);
	}
	
	public void setBulletSize(int bulletSize) { //대포알 수 지정 메소드
		this.bulletSize=bulletSize;
	}
	public void startCannonThread(int count) { //인자 : 대포알 갯수
		bulletVector=new Vector<Bullet>(); //벡터 객체를 새로 주지 않으면 벡터 객체에 
		//저장되어있는 Bullet 객체들이 스레드가 진행중인 채로 새로운 스레드에서 또 접근을 한다. 물론 전의 스레드를 아래의 cannonThread.setInterrupt()로 삭제할수있지만 벡터 초기화는 기본으로 가져가기
		//그러면 스레드가 여러번 실행되며 이동속도가 훨씬 빨라보이게 된다. 조심할것
		
		score=0; //스코어 초기화된 상태로 스레드 시작
		if(cannonThread!=null) cannonThread.setInterrupt(); //버튼을 눌렀을 때 cannonThread가 존재한다면 (전 판을 했었다면) cannonThread에 interrupt를 해준다
		int height1=(int)(Math.random()*(height-100))/10;
		int height2=height1*10;
		target=new Target(new Point(width/2+200,height2),new Point(width/2+200,height/2)); // 새로운 타겟 객체를 생성한다.
		
		targetThread=new TargetThread(target,this);
		targetThread.start();
		cannonThread=new CannonThread(this,bulletVector,target,count,x,y); //인자로 Vector넘겨주며 스레드 생성
		cannonThread.start();
	}
	public int getScore() { // 현재 점수 반환
		return score;
	}
	public void setScore(int score) { // 현재 점수 세팅
		this.score=score;
	}
	public void setScoreLabel() { // 점수 Label을 설정한다
		scoreLabel.setText(Integer.toString(score));
	}
	public void setScoreZero() { // 현재 점수를 0점으로 초기화
		scoreLabel.setText(Integer.toString(0));
	}
	public void setBeforeCount() { //게임이 끝나면 이전 게임 점수로 반영시켜주는 메소드
		beforeScoreLabel.setText(Integer.toString(score));
	}
	public Tank getTank() { //Tank 객체의 레퍼런스를 반환해주는 메소드
		return tank;
	}
	@Override
	public void paintComponent(Graphics g) { //이 패널에서 게임이 그려진다.
		super.paintComponent(g);
		g.setColor(new Color(64,151,93));
		
		g.fillRect(tank.getX()-10,tank.getY(),60,20); //탱크 그리기
		g.fillRect(tank.getX()+10,tank.getY()-10,20,10); //탱크 그리기
		g.setColor(Color.BLACK);
		g.fillOval(tank.getX()-13,tank.getY()+15,10,10); //탱크 그리기
		g.fillOval(tank.getX()-4,tank.getY()+15,10,10); //탱크 그리기
		g.fillOval(tank.getX()+5,tank.getY()+15,10,10); //탱크 그리기
		g.fillOval(tank.getX()+14,tank.getY()+15,10,10); //탱크 그리기
		g.fillOval(tank.getX()+23,tank.getY()+15,10,10); //탱크 그리기
		g.fillOval(tank.getX()+32,tank.getY()+15,10,10); //탱크 그리기
		g.fillOval(tank.getX()+41,tank.getY()+15,10,10); //탱크 그리기
		//bullet 그리기
		
		g.setColor(new Color(90,90,90));
		for(int i=0;i<bulletVector.size();i++) {
			Bullet b=bulletVector.get(i);
			g.fillOval(b.getX(),b.getY(),b.getSize(),b.getSize());
		}
		//g.fillOval(bullet.getX(),bullet.getY(),bullet.getSize(),bullet.getSize());
		
		g.setColor(Color.BLACK);
		if(target!=null) g.fillRect(target.getStartPoint().x,target.getStartPoint().y,20,100);
		
	}
}

class Tank{
	private int x; //탱크의 x좌표
	private int y; //탱크의 y좌표
	public Tank(int x,int y) {
		this.x=x;
		this.y=y;
	} //탱크의 x y좌표 초기화
	public int getX() { //x 좌표 반환
		return x;
	}
	public int getY() { //y 좌표 반환
		return y;
	}
	public void setX(int x) { //x 좌표 변경
		this.x=x;
	}
	public void setY(int y) { //y 좌표 변경
		this.y=y;
	}
}
class Bullet{
	private int x; //포의 x좌표
	private int y; //포의 y좌표
	private int size; //포의 size
	
	public Bullet(int x,int y,int size) { //포의 x , y 좌표 . size 초기화
		this.x=x;
		this.y=y;
		this.size=size;
	}
	public int getX() { //x 좌표 반환
		return x;
	}
	public int getY() { //y 좌표 반환
		return y;
	}
	public void setX(int x) { //x 좌표 변경
		this.x=x;
	}
	public void setY(int y) { //y 좌표 변경
		this.y=y;
	}
	public int getSize() { //대포알의 사이즈를 반환
		return size;
	}
}
class Target {
	private Point p1; //target 시작 지점
	private Point p2; //target 끝 지점
	public Target(Point p1,Point p2) {
		this.p1=p1;
		this.p2=p2;
	}
	public Point getStartPoint() { //Target의 시작 포인트 지점
		return p1;
	}
	public Point getEndPoint() { //Target의 끝 포인트 지점
		return p2;
	}
	public void setPoint(Point p) { //Target의 포인트를 바꿔주는 메소드
		this.p1=p;
	}
}
import java.util.Vector;

public class CannonThread extends Thread{
	
	private boolean flag=false; // if flag==true -> 스레드 재움
	private GamePanel gamePanel;
	private Target target;
	private Vector<Bullet> bulletVector; //벡터를 넘겨 받고 , 스레드 진행
	private int x; //포탄의 시작 x좌표
	private int y; //포탄의 시작 y좌표
	private int count; //대포알 갯수
	private int count_copy; //대포알 갯수 복사
	private int count1=0; //스레드를 하나 더 사용하지 않고 한 스레드로 대포알 하나가 생성되는
	//시간을 조정하도록 해봄
	private boolean count_up=true; //false 가 되면 더이상 대포알 객체 벡터에 저장하지 않음
	public CannonThread(GamePanel gamePanel,Vector<Bullet> bulletVector,Target target,int count,int x,int y) { //인자로 포의 각도 , 포의 갯수 , 포의 사거리 등 설정
		this.gamePanel=gamePanel;
		this.bulletVector=bulletVector;
		this.count=count;
		this.count_copy=count;
		this.target=target;
		this.x=x;
		this.y=y;
	}
	
	private void moveBullet() {
		if(count_up==true) {
			count1++; //10이 되면 대포알 하나 더 발사
		}
		if(count1%10==0) {
			count_copy-=1; //남은 발사 가능한 볼렛의 수가 줄어들고
			count1=0;
			if(count_copy<0) { //남은 발사 가능한 대포알의 수가 0보다 작다면
				count1=0; //count1은 0으로 바꾸어주지 않아도 딱히 상관은 없음
				count_up=false; //count_up을 false로 바꾸어 더이상 bulletVector에 Bullet객체를 생성하지 않도록 한다
				
			}
			else {
				int y=gamePanel.getTank().getY();
				bulletVector.add(new Bullet(x,y,10));
			}
			
		}
		for(int i=0;i<bulletVector.size();i++) {
			Bullet b=bulletVector.get(i);
			b.setX(b.getX()+10); //이 부분을 좀 바꿔줘야한다. 곡선으로 날아가게끔
			//b.setY(); 이부분도 같이 바꾸어 주어야한다. x , y 좌표를 Math클래스의 메소드를 사용하면 괜찮을 것으로 예상
		}
		
		for(int i=0;i<bulletVector.size();i++) {
			Bullet b=bulletVector.get(i);
			if((b.getX()+10>=target.getStartPoint().x && b.getX()<target.getStartPoint().x+10) && (b.getY()>target.getStartPoint().y && b.getY()<target.getStartPoint().y+100)) {
				bulletVector.remove(i);
				gamePanel.setScore(gamePanel.getScore()+1); //점수 증가
				System.out.print(gamePanel.getScore());
				gamePanel.setScoreLabel();
			}
		}
		if(gamePanel.getScore()==count) {
			gamePanel.setBeforeCount();
			gamePanel.setScoreZero(); //점수 초기화
		}
		gamePanel.repaint();
		gamePanel.revalidate();

	}
	public void setInterrupt() {
		this.interrupt();
	}
	synchronized public void notifyThread() { //다른 클래스에서 스레드 깨우는데 사용
		this.notify();
	}
	public void threadWait() {
		flag=true; //스레드 재우기
	}
	synchronized private void waitThread() {
		try {
			this.wait();
		}catch(Exception e) {
			System.out.print("wait하는 중 오류 발생");
		}
	}
	@Override
	public void run() {
		while(true) {
			try {
				if(flag==true) { //flag가 true라면
					waitThread(); //스레드 재우기
				}
				moveBullet(); //bullet 이동 메소드 호출
				sleep(10);
			}catch(InterruptedException e) {
				System.out.println("interrupt 발생");
				break;
			}
		}
	}
}
import java.awt.Point;

public class TargetThread extends Thread{
	Target target;
	GamePanel gamePanel;
	int direction=1; //방향
	public TargetThread(Target target,GamePanel gamePanel) {
		this.target=target;
		this.gamePanel=gamePanel;
	}
	
	private void targetMove() {
		if(target.getStartPoint().y==10) {
			direction=1;
		}
		else if(target.getStartPoint().y==250){
			direction=-1;
		}
		Point p=target.getStartPoint();
		
		target.setPoint(new Point(p.x,p.y+(direction*10)));
		gamePanel.repaint();
		gamePanel.revalidate();
	}
	
	
	@Override
	public void run() {
		while(true) {
			try {
				targetMove();
				sleep(100);
			}catch(Exception e) {
				System.out.println("target 스레드 Interrupt 발생");
			}
		}
	}
}

게임 실행 화면

게임 실행 시 모습

게임 진행중 모습

게임이 끝났을 때의 모습

게임 재시작 시 전판 점수는 이전게임 점수로 보이는 모습

이 프로젝트를 진행하면서 처음에는 무난하게 진행이 되었는데 , 스레드를 하나로(일정 시간마다 벡터에 Bullet 객체를 저장하는 스레드 , 저장되어있는 Bullet 객체를 제어하는 스레드 두개의 스레드로 코드를 짜는게 훨씬 관리도 편했을 텐데) 짜는 순간부터 조금씩 코드가 꼬이기 시작했다. 다음에 유사한 프로젝트 진행 시에는 스레드를 나누어서 진행해야할 것 같다.


테트리스 실행결과

그리고 전에 올렸던 게시글에서 테트리스 코드를 완성시키고 올리려했는데 , 코드가 너무 길어져 실행 화면 위주로 올려보려 한다.

좌측에는 게임이 진행되고 , 우측 상단에는 일시정지버튼과 게임 재개버튼이 보인다. 우측 하단에는 다음에 내려올 블록의 정보를 확인할 수 있다.

버튼을 눌러 일시정지 했을때의 모습이다. 게임 재개시 , 게임 재개버튼에 Focus가 이동하므로 gamePanel.requestFocus();를 해주어야 다시 키가 먹는다.

다시 게임을 재개했을 때 모습이다.

블록이 천장에 닿기 전

천장에 닿게되면 Game Over문구가 나오며 게임이 종료된다. 다시시작 버튼을 눌러 재시작 할 수 있다.

이상으로 코딩캠프에서의 3박 4일동안 제작한 미니 프로젝트였습니다. 감사합니다.

profile
💻 🍎

0개의 댓글