Mozzida
article thumbnail
Published 2022. 10. 11. 01:42
BubbleBubble-final Java

길고 길었던 Bubble Bubble 프로젝트가 끝이 났다. 사실 완벽하게 끝난건 아니다  

Bubble쪽 List로 받은 내용도 삭제해야 하는데.. 일단 마무리된거만 올릴려고 한다. 

필기는 이때까지 한거 다 적어뒀으니 참고 하실분들은 참고 하세요.

 

BubbleFrame.java

<java />
package bubble.test.ex18; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import javax.swing.ImageIcon; import javax.swing.JFrame; import javax.swing.JLabel; import lombok.Getter; import lombok.Setter; @Getter @Setter public class BubbleFrame extends JFrame { private BubbleFrame mContext = this; private JLabel backgroundMap; private Enemy enemy; private Player player; public BubbleFrame() { // 생성자 만들기 initObject(); initSetting(); initListener(); // listener 추가 setVisible(true); } private void initObject() { backgroundMap = new JLabel(new ImageIcon("image/backgroundMap.png")); // label 안에 이미지 넣음 setContentPane(backgroundMap); // backgroundMap.setSize(100,100); // backgroundMap.setLocation(300,300); // backgroundMap.setSize(1000,600); // add(backgroundMap);// JFrame에 JLabel이 그려진다. player = new Player(mContext); // Player함수를 호출하여 BubbleFrame에 추가한다 add(player); enemy = new Enemy(mContext); add(enemy); new BGM(); } private void initSetting() { setSize(1000, 640); setLayout(null);// absolute 레이아웃 (자유롭게 그림을 그릴 수 있다.) setLocationRelativeTo(null); // JFrame 가운데 배치하기 setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // x버튼으로 창을 끌 때 JVM 같이 종료 하기 } private void initListener() { // this 이친구의 this addKeyListener(new KeyAdapter() { // 람다식으로 Interface를 가져올시 전부 기능을 정의 해야함 하지만 Adapter를 사용해서 정의가 가능하다 // 키보드 클릭 이벤트 핸들러 @Override public void keyPressed(KeyEvent e) { // System.out.println(e.getKeyCode()); // 실행하여 방향키를 입력해본다 // 39 오른쪽 // 37 왼쪽 숫자로 표현하면 알아보기 힘들다 KeyEvent.VK_ 를 활용하여 알기 쉽게 한다 // 40 밑 // 38 위 switch (e.getKeyCode()) { case KeyEvent.VK_LEFT: if (!player.isLeft() && !player.isLeftWallCrash()) { player.left(); } break; case KeyEvent.VK_RIGHT: if (!player.isRight() && !player.isRightWallCrash()) { player.right(); } break; case KeyEvent.VK_UP: if (!player.isUp() && !player.isDown()) { player.up(); } break; case KeyEvent.VK_SPACE: //버블의 주체는 player인데 bubbleFrame에서 지금 new를 하고 있다. // System.out.println("발싸!"); // Bubble bubble = new Bubble(mContext); // this 익명 클래스의 정보 // add(bubble); player.attack(); break; } } // case KeyEvent.VK_DOWN: 떨어지거나 할때 사용하는데 따로 down키를 눌릴 일이 없다 // player.down(); // break; // 키보드 해제 이벤트 핸들러 @Override public void keyReleased(KeyEvent e) { switch (e.getKeyCode()) { case KeyEvent.VK_LEFT: player.setLeft(false); break; case KeyEvent.VK_RIGHT: player.setRight(false); break; } } }); } public static void main(String[] args) { new BubbleFrame(); } }

Moveable.java

<java />
package bubble.test.ex18; public interface Moveable { public abstract void left(); public abstract void right(); public abstract void up(); default public void down() {}; default public void attack() {}; // public이 아닌이유 버블은 공격의 주체가 아니다 -> 공격은 player가 하는것 // 자바 높은 버전부터 나온 문법 // default를 사용하면 인터페이스도 몸체가 있는 메서드를 만들수 있다. (다중 상속이 안되는 것이 많기 때문에) // 그래서 어댑터 패턴보다는 default를 사용하는것이 좋다 . }

Player.java

<java />
package bubble.test.ex18; import java.util.ArrayList; import java.util.List; import javax.swing.ImageIcon; import javax.swing.JLabel; import lombok.Data; import lombok.Getter; import lombok.Setter; // class player -> new 가능한 애들!! 게임에 존재할 수 있음. (추상메서드를 가질 수 없다.) @Setter @Getter public class Player extends JLabel implements Moveable { // player 자체가 label이다 private BubbleFrame mContext; // 못받아 오니BubbleFrame 호출해야한다 private List<Bubble> bubbleList; // 위치 상태 private int x; private int y; // 플레이어의 방향 private PlayerWay playerWay; // 움직임 상태 private boolean left; private boolean right; private boolean up; private boolean down; // 벽에 충돌한 상태 private boolean leftWallCrash; private boolean rightWallCrash; // 플레이어 속도 상태 private final int SPEED = 4; // 상수처리 private final int JUMP = 2; // up, down private ImageIcon playerR, playerL; public Player(BubbleFrame mContext) { this.mContext = mContext; initObject(); // 오브젝트 세팅 initSetting(); initBackgroundPlayerService(); } private void initObject() { playerR = new ImageIcon("image/playerR.png"); // 이미지 ImageIcon에 사진 추가 playerL = new ImageIcon("image/playerL.png"); bubbleList = new ArrayList<>(); } private void initSetting() { // 기본 시작 위치 x = 80; y = 535; // 최초 상태 -> 움직인다 (좌표가 이동한다) // 키보드 오른쪽을 누른다 -> 오른쪽으로 간다 left = false; right = false; up = false; down = false; leftWallCrash = false; rightWallCrash = false; playerWay = PlayerWay.RIGHT; setIcon(playerR); setSize(50, 50); setLocation(x, y); } private void initBackgroundPlayerService() {// Thread 사용시 Runable 사용해야하지만 BackgroundPlayerService자체가 Runable 상속 받았기 // 때문에 바로 가능 new Thread(new BackgroundPlayerService(this)).start(); } @Override public void attack() { // Moveable에서 attack을 호출한다. new Thread(() -> { // 어택 호출시 쓰레드 실행한다. Bubble bubble = new Bubble(mContext); // Thread가 시작되면서 bubble이 만들어 지고 왼쪽 아니면 오른쪽으로만 이동하면 된다 mContext.add(bubble); bubbleList.add(bubble); if (playerWay == playerWay.LEFT) { bubble.left(); } else { bubble.right(); } }).start(); } // 이벤트 핸들러 @Override public void left() { playerWay = PlayerWay.LEFT; left = true; new Thread(() -> { // Thread 생성 while (left) { x = x - SPEED; // x축 1만큼 - 이동 setIcon(playerL); // 왼쪽으로 갈때는 왼쪽 이미지 setLocation(x, y); try { Thread.sleep(10);// 0.01초 } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); // Thread에는 Runable의 익명 클래스가 필요하다 } @Override public void right() { playerWay = PlayerWay.RIGHT; right = true; new Thread(() -> { while (right) { setIcon(playerR); // 왼쪽으로 갈때는 오른쪽 이미지 x = x + SPEED; // x축 1만큼 +이동 setLocation(x, y); try { Thread.sleep(10);// 0.01초 } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); // Thread에는 Runable의 익명 클래스가 필요하다 } // left + up , right + up 가능하다 Thread가 없으면 불가능 함 @Override public void up() { System.out.println("짬푸"); up = true; new Thread(() -> { for (int i = 0; i < 130 / JUMP; i++) { // JUMP 변경시에도 알아보기 좋다. 나중에 복잡해진다 y = y - JUMP; // y값을 - 하는게 점프 setLocation(x, y); try { Thread.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } } up = false; down(); }).start(); } @Override public void down() { // System.out.println("down"); down = true; new Thread(() -> { while (down) { // JUMP 변경시에도 알아보기 좋다. 나중에 복잡해진다 // for문 쓰면 부조건 바닥에 떨어짐 while문 변경 -> 떨어지다가 false로 바뀌면서 멈춤 y = y + JUMP; // y값을 - 하는게 점프 setLocation(x, y); try { Thread.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } } down = false; }).start(); } }

PlayerWay.java(Enum)

<java />
package bubble.test.ex18; public enum PlayerWay { LEFT,RIGHT; }

BackgroundPlayerService.java

<java />
package bubble.test.ex18; import java.awt.Color; import java.awt.image.BufferedImage; import java.io.File; import java.util.List; import javax.imageio.ImageIO; // 메인 쓰레드 바쁨 - 키보드 이벤트를 처리하기 바쁨. // Background에서 플레이어의 움직임을 관찰하는 Class public class BackgroundPlayerService implements Runnable { private BufferedImage image; // image를 Buffered로 읽어야 test 가능 private Player player; private List<Bubble> bubbleList; // 전체 리스트를 가져와서 계산을 한다. // 플레이어, 버블 public BackgroundPlayerService(Player player) { this.player = player; this.bubbleList = player.getBubbleList(); try { image = ImageIO.read(new File("image/backgroundMapService.png")); } catch (Exception e) { System.out.println(e.getMessage()); } } @Override public void run() { while (true) { // 1. 버블 충돌 체크 for (int i = 0; i < bubbleList.size(); i++) { Bubble bubble = bubbleList.get(i); if (bubble.getState() == 1) { if ((Math.abs(player.getX() - bubble.getX()) < 10) && (Math.abs(player.getY() - bubble.getY()) > 0 && Math.abs(player.getY() - bubble.getY()) < 50)) { System.out.println("적군 사살 완료"); bubble.clearBubbled(); break; } } } // 2. 벽 충돌 체크 // 색상 확인 -> 부딪치는곳 영역 잡기 위해서 사용중 Color leftColor = new Color(image.getRGB(player.getX() - 10, player.getY() + 25)); Color rightColor = new Color(image.getRGB(player.getX() + 50 + 15, player.getY() + 25)); // -2가 나온다는 뜻은 바닥에 색깔이 없이 흰색 int bottomColor = image.getRGB(player.getX() + 10, player.getY() + 50 + 5) // 가장 왼쪽 하단 = -1 + image.getRGB(player.getX() + 40, player.getY() + 50 + 5); // 가장 오른쪽 하단 = -1 // System.out.println("bottomColor" + bottomColor); // System.out.println("leftColor : " + leftColor); // System.out.println("rightColor : " + rightColor); // Color color = new Color(image.getRGB(player.getX(), player.getY())); // System.out.println("색상 : " + color); // 바닥 충돌 확인 if (bottomColor != -2) { // System.out.println("바닥에 충돌함"); player.setDown(false); } else { // -2 일때 실행됨 => 내 바닥 색깔이 하얀색이라는 것 if (!player.isUp() && !player.isDown()) { // player가 isUP + is Down 상태가 아닐때 player.down(); } } // 외벽 충돌 확인 if (leftColor.getRed() == 255 && leftColor.getGreen() == 0 && leftColor.getBlue() == 0) { // RGB 빨간색 // System.out.println("왼쪽 벽에 충돌함"); player.setLeftWallCrash(true); player.setLeft(false); } else if (rightColor.getRed() == 255 && rightColor.getGreen() == 0 && rightColor.getBlue() == 0) { // System.out.println("오른쪽 벽에 충돌함"); player.setRightWallCrash(true); player.setRight(false);// 이것만 하면 되는줄 알았지만 계속 누르게 되면 뜷고 들어가게 된다. } else { player.setLeftWallCrash(false); player.setRightWallCrash(false); } try { // sleep 없으면 너무 빨리 실행된다. Thread.sleep(10); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }

Bubble.Java

<java />
package bubble.test.ex18; import javax.swing.ImageIcon; import javax.swing.JLabel; import lombok.Getter; import lombok.Setter; @Getter @Setter public class Bubble extends JLabel implements Moveable { // 의존성 콤포지션 -> bubble은 player에 의존적이다 private BubbleFrame mContext; private Player player; private Enemy enemy; // 적군이 여러명이면 Enemy 컬렉션으로 만들면 된다. private BackgroundbubblerService backgroundbubblerService; // bubble이 만들어질때마다 new // 위치 상태 private int x; private int y; // 움직임 상태 private boolean left; private boolean right; private boolean up; // private boolean down; 방울은 밑으로 못간다 // 적군을 맞춘 상태 private int state; // 0(물방울), 1(적을 가둔 물방울) private ImageIcon bubble; // 물방울 private ImageIcon bubbled; // 적을 가둔 물방울 private ImageIcon bomb; // 물방울이 터진 상태 public Bubble(BubbleFrame mContext) { // player를 넣어줘야 방울의 위치를 잡아 줄수 있다 this.mContext = mContext; // 모든정보를 가진 Context가 있으니까 플레이어 정보를 들고 올수 있다. this.player = mContext.getPlayer(); this.enemy = mContext.getEnemy(); initObject(); initSetting(); // initThread(); } private void initSetting() { left = false; right = false; up = false; x = player.getX(); y = player.getY(); // bubble의 방향이 결정되어야 한다. setIcon(bubble); setSize(50, 50); state = 0; } private void initObject() { bubbled = new ImageIcon("image/bubbled.png"); bubble = new ImageIcon("image/bubble.png"); bomb = new ImageIcon("image/bomb.png"); backgroundbubblerService = new BackgroundbubblerService(this); } // private void initThread() { // // 버블은 쓰레드가 하나만 필요하다. // new Thread(()->{ // if(player.getPlayerWay() == PlayerWay.LEFT) { // left(); // } else { // right(); // } // }).start(); // } @Override public void left() { left = true; for (int i = 0; i < 400; i++) { x--; setLocation(x, y); if (backgroundbubblerService.leftWall()) { left = false; break; } // 40과 60의 범위 절대값 if ((Math.abs(x - enemy.getX()) < 10) && (Math.abs(y - enemy.getY()) > 0 && Math.abs(y - enemy.getY()) < 50)) { System.out.println("물방울이 적군과 충돌하였습니다."); if (enemy.getState() == 0) { attack(); break; } } try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } up(); } @Override public void right() { right = true; for (int i = 0; i < 400; i++) { x++; setLocation(x, y); if (backgroundbubblerService.rightWall()) { right = false; break; } // 아군과 적군의 거리가 10의 차이가 나면 if ((Math.abs(x - enemy.getX()) < 10) && (Math.abs(y - enemy.getY()) > 0 && Math.abs(y - enemy.getY()) < 50)) { System.out.println("물방울이 적군과 충돌하였습니다."); if (enemy.getState() == 0) { attack(); break; } } try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } up(); } @Override public void up() { up = true; while (up) { y--; setLocation(x, y); if (backgroundbubblerService.topWall()) { up = false; break; // break 필요는 없지만 이단계에서 바로 체크가 되어서 2번 안돌아도 되니까 break 사용함 } try { if (state == 0) { // 기본 물방울 Thread.sleep(1); } else { // 적은 가둔 물방울 Thread.sleep(10); } } catch (InterruptedException e) { e.printStackTrace(); } } if (state == 0) clearBubble(); // 천장에 버블이 도착하고 나서 3초 후에 메모리에서 소멸 } @Override public void attack() { state = 1; enemy.setState(1); setIcon(bubbled); mContext.remove(enemy); // 메모리에서 사라지게 한다. (가비지 컬렉션 -> 즉시 발동하지 않음) mContext.repaint(); // 깔끔하게 안사라져서 리페인트 작업 해야한다. 화면갱신 } // 행위 -> clear (동사) -> bubble (목적어) private void clearBubble() {// 다른곳에서 호출할일이 없기 때문에 private 사용 try { Thread.sleep(3000); setIcon(bomb); Thread.sleep(500); mContext.remove(this); // bubbleFrame 의 bubble이 메모리에서 소멸된다. mContext.repaint(); // bubbleFrame 의 전체를 다시 그린다.(메모리에서 없는 건 안그린다.) } catch (InterruptedException e) { e.printStackTrace(); } } public void clearBubbled() { new Thread(()->{ System.out.println("clearBubbled"); try { up = false; setIcon(bomb); Thread.sleep(1000); mContext.remove(this); mContext.repaint(); } catch (Exception e) { e.printStackTrace(); } }).start(); } }

BackgroundbubblerService.java

<java />
package bubble.test.ex18; import java.awt.Color; import java.awt.image.BufferedImage; import java.io.File; import javax.imageio.ImageIO; public class BackgroundbubblerService {// bubble이 만들어질때마다 유지 되어야한다. -> 버블은 언제 만들어지나?(만들어지는 시점) -> VK_SPACE에서 private BufferedImage image; private Bubble bubble; public BackgroundbubblerService(Bubble bubble) { this.bubble = bubble; try { image = ImageIO.read(new File("image/backgroundMapService.png")); } catch (Exception e) { } } public boolean leftWall() { Color leftColor = new Color(image.getRGB(bubble.getX() - 10, bubble.getY() + 25)); // 외벽 충돌 확인 if (leftColor.getRed() == 255 && leftColor.getGreen() == 0 && leftColor.getBlue() == 0) { return true; } return false; } public boolean rightWall() { Color rightColor = new Color(image.getRGB(bubble.getX() + 50 + 15, bubble.getY() + 25)); if (rightColor.getRed() == 255 && rightColor.getGreen() == 0 && rightColor.getBlue() == 0) { return true; } return false; } public boolean topWall() { Color topColor = new Color(image.getRGB(bubble.getX()+25, bubble.getY()-10)); if (topColor.getRed() == 255 && topColor.getGreen() == 0 && topColor.getBlue() == 0) { return true; } return false; } }

 

Enemy.Java

<java />
package bubble.test.ex18; import javax.swing.ImageIcon; import javax.swing.JLabel; import lombok.Data; import lombok.Getter; import lombok.Setter; // class player -> new 가능한 애들!! 게임에 존재할 수 있음. (추상메서드를 가질 수 없다.) @Getter @Setter public class Enemy extends JLabel implements Moveable { // player 자체가 label이다 private BubbleFrame mContext; // 못받아 오니BubbleFrame 호출해야한다 // 위치 상태 private int x; private int y; // 적군의의 방향 private EnemyWay enemyWay; // 움직임 상태 private boolean left; private boolean right; private boolean up; private boolean down; private int state; // 0(살아있는 상태), 1(물방울에 갇힌 상태) // 적군의 속도 상태 private final int SPEED = 3; // 상수처리 private final int JUMP = 1; // up, down private ImageIcon enemyR, enemyL; public Enemy(BubbleFrame mContext) { this.mContext = mContext; initObject(); // 오브젝트 세팅 initSetting(); initBackgroundEnemyService(); right(); } private void initObject() { enemyR = new ImageIcon("image/enemyR.png"); // 이미지 ImageIcon에 사진 추가 enemyL = new ImageIcon("image/enemyL.png"); } private void initSetting() { // 기본 시작 위치 x = 480; y = 178; // 최초 상태 -> 움직인다 (좌표가 이동한다) // 키보드 오른쪽을 누른다 -> 오른쪽으로 간다 left = false; right = false; up = false; down = false; state = 0; enemyWay = enemyWay.RIGHT; setIcon(enemyR); setSize(50, 50); setLocation(x, y); } private void initBackgroundEnemyService() {// Thread 사용시 Runable 사용해야하지만 BackgroundPlayerService자체가 Runable 상속 받았기 // 때문에 바로 가능 new Thread(new BackgroundEnemyService(this)).start(); } // 이벤트 핸들러 @Override public void left() { enemyWay = EnemyWay.LEFT; left = true; new Thread(() -> { // Thread 생성 while (left) { x = x - SPEED; // x축 1만큼 - 이동 setIcon(enemyL); // 왼쪽으로 갈때는 왼쪽 이미지 setLocation(x, y); try { Thread.sleep(10);// 0.01초 } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); // Thread에는 Runable의 익명 클래스가 필요하다 } @Override public void right() { enemyWay = EnemyWay.RIGHT; right = true; new Thread(() -> { while (right) { setIcon(enemyR); // 왼쪽으로 갈때는 오른쪽 이미지 x = x + SPEED; // x축 1만큼 +이동 setLocation(x, y); try { Thread.sleep(10);// 0.01초 } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); // Thread에는 Runable의 익명 클래스가 필요하다 } @Override public void up() { up = true; new Thread(() -> { for (int i = 0; i < 130 / JUMP; i++) { // JUMP 변경시에도 알아보기 좋다. 나중에 복잡해진다 y = y - JUMP; // y값을 - 하는게 점프 setLocation(x, y); try { Thread.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } } up = false; down(); }).start(); } @Override public void down() { // System.out.println("down"); down = true; new Thread(() -> { while (down) { // JUMP 변경시에도 알아보기 좋다. 나중에 복잡해진다 // for문 쓰면 부조건 바닥에 떨어짐 while문 변경 -> 떨어지다가 false로 바뀌면서 멈춤 y = y + JUMP; // y값을 - 하는게 점프 setLocation(x, y); try { Thread.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } } down = false; }).start(); } }

EnemyWay.java(Enum)

<java />
package bubble.test.ex18; public enum EnemyWay { LEFT,RIGHT; }

BackgroundEnemyService.java

<java />
package bubble.test.ex18; import java.awt.Color; import java.awt.image.BufferedImage; import java.io.File; import java.util.List; import javax.imageio.ImageIO; // 메인 쓰레드 바쁨 - 키보드 이벤트를 처리하기 바쁨. // Background에서 플레이어의 움직임을 관찰하는 Class public class BackgroundEnemyService implements Runnable { private BufferedImage image; // image를 Buffered로 읽어야 test 가능 private Enemy enemy; // 플레이어, 버블 public BackgroundEnemyService(Enemy enemy) { this.enemy = enemy; try { image = ImageIO.read(new File("image/backgroundMapService.png")); } catch (Exception e) { System.out.println(e.getMessage()); } } @Override public void run() { while (enemy.getState() == 0) { Color leftColor = new Color(image.getRGB(enemy.getX() - 10, enemy.getY() + 25)); Color rightColor = new Color(image.getRGB(enemy.getX() + 50 + 15, enemy.getY() + 25)); int bottomColor = image.getRGB(enemy.getX() + 10, enemy.getY() + 50 + 5) // 가장 왼쪽 하단 = -1 + image.getRGB(enemy.getX() + 40, enemy.getY() + 50 + 5); // 가장 오른쪽 하단 = -1 if (bottomColor != -2) { // System.out.println("바닥에 충돌함"); enemy.setDown(false); } else { // -2 일때 실행됨 => 내 바닥 색깔이 하얀색이라는 것 if (!enemy.isUp() && !enemy.isDown()) { // player가 isUP + is Down 상태가 아닐때 enemy.down(); } } // 외벽 충돌 확인 if (leftColor.getRed() == 255 && leftColor.getGreen() == 0 && leftColor.getBlue() == 0) { // RGB 빨간색 enemy.setLeft(false); if(!enemy.isRight()) { enemy.right(); } } else if (rightColor.getRed() == 255 && rightColor.getGreen() == 0 && rightColor.getBlue() == 0) { enemy.setRight(false);// 이것만 하면 되는줄 알았지만 계속 누르게 되면 뜷고 들어가게 된다. if(!enemy.isLeft()) { enemy.left(); } } try { // sleep 없으면 너무 빨리 실행된다. Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } } }

 

BGM.java

<java />
package bubble.test.ex18; import java.io.File; import java.io.IOException; import javax.sound.sampled.AudioInputStream; import javax.sound.sampled.AudioSystem; import javax.sound.sampled.Clip; import javax.sound.sampled.FloatControl; import javax.sound.sampled.UnsupportedAudioFileException; public class BGM { public BGM() { try { AudioInputStream ais = AudioSystem.getAudioInputStream(new File("sound/bgm.wav")); Clip clip = AudioSystem.getClip(); clip.open(ais); // 소리 설정 FloatControl gainControl = (FloatControl) clip.getControl(FloatControl.Type.MASTER_GAIN); // 볼륨 조정 gainControl.setValue(-30.0f); clip.start(); } catch (Exception e) { e.printStackTrace(); } } }

환호의 빠뽀먼스 완성!!

힘들었습니다. 난중에 보강해서 다시 올리겠습니다 쓩!

profile

Mozzida

@Mozzida

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!