구너드 2023. 6. 1. 17:06

여태 배운 Java 문법들을 복습한다는 느낌으로 클론 코딩을 진행했다.

참고한 강의는 

 

https://youtu.be/xs92kqU2YWg


package Drop_the_Beat14;

public class  Main {

    public static final int SCREEN_WIDTH = 1280;
    public static final int SCREEN_HEIGHT = 720;
    public static final int NOTE_SPEED = 3;
    public static final int SLEEP_TIME = 10;
    public static final int REACH_TIME = 2;

    public static void main(String[] args) {

        new DropTheBeat();
        
    }
}

드랍 더 비트라고 이름을 지은 리듬게임.

Main 클래스에 변하지 않는 값을 상수로 지정.


    private Image screenImage;
    private Graphics screenGraphic;

    private ImageIcon exitButtonBasicImage = new ImageIcon(Main.class.getResource("../images/ExitButtonBasic.png"));
    private ImageIcon exitButtonEnteredImage = new ImageIcon(Main.class.getResource("../images/ExitButtonEntered.png"));
    private ImageIcon startButtonEnteredImage = new ImageIcon(Main.class.getResource("../images/startButtonEntered.png"));
    private ImageIcon startButtonBasicImage = new ImageIcon(Main.class.getResource("../images/startButtonBasic.png"));
    private ImageIcon quitButtonEnteredImage = new ImageIcon(Main.class.getResource("../images/quitButtonEntered.png"));
    private ImageIcon quitButtonBasicImage = new ImageIcon(Main.class.getResource("../images/quitButtonBasic.png"));
    private ImageIcon leftButtonEnteredImage = new ImageIcon(Main.class.getResource("../images/leftButtonEntered.png"));
    private ImageIcon leftButtonBasicImage = new ImageIcon(Main.class.getResource("../images/leftButtonBasic.png"));

게임에 쓰이는 각종 이미지들을 인텔리제이 안으로 들여오는 과정.

각 이미지 아이콘이 어떤 이미지를 가져올건지 getResoursce() 메서드를 통해 정의해준다.

이 외에도, 메뉴바, 백그라운드 이미지들도 있지만 코드가 너무 길어지므로 생략


    public DropTheBeat () {
        trackList.add(new Track("Light Title Image.png", "Light Start Image.png", "Light Image.png", "Lights Selected.mp3", "Light - Walen.mp3", "Light - Walen"));
        trackList.add(new Track("Beautiful Liar Title Image.png", "Beautiful Liar Start Image.png", "Beautiful Liar Image.png", "Beautiful Liar Selected.mp3", "Beautiful Liar - Markvard & AgusAlvarez.mp3","Beautiful Liar - Markvard & AgusAlvarez"));
        trackList.add(new Track("Cuba Title Image.png", "Cuba Start Image.png", "Cuba Image.png", "Cuba Selected.mp3", "Cuba - ASHUTOSH.mp3", "Cuba - ASHUTOSH"));


        setUndecorated(true);
        setTitle("Drop the Beat");
        setSize(Main.SCREEN_WIDTH, Main.SCREEN_HEIGHT);
        setResizable(false);
        setLocationRelativeTo(null);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setVisible(true);
        setBackground(new Color(0, 0, 0, 0));
        setLayout(null);

        addKeyListener(new KeyListener());

        intromusic.start();


        exitButton.setBounds(1250, 0, 30, 30);
        exitButton.setBorderPainted(false);
        exitButton.setContentAreaFilled(false);
        exitButton.setFocusPainted(false);
        exitButton.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseEntered(MouseEvent e) {
                exitButton.setIcon(exitButtonEnteredImage);
                exitButton.setCursor(new Cursor(Cursor.HAND_CURSOR));
                Music ButtonEnteredMusic = new Music("ButtonEnteredMusic.mp3", false);
                ButtonEnteredMusic.start();
            }
            @Override
            public void mouseExited(MouseEvent e) {
                exitButton.setIcon(exitButtonBasicImage);
                exitButton.setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
            }
            @Override
            public void mousePressed(MouseEvent e) {
                Music ButtonEnteredMusic = new Music("ButtonPressedMusic.mp3", false);
                ButtonEnteredMusic.start();
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException ex) {
                    ex.printStackTrace();
                }
                System.exit(0);
            }
        });
        add(exitButton);

DroptheBeat 클래스

각 이미지들이 어떤 기능을 할지 부여해주고 게임의 컨텐츠들이 순차적으로 화면에 나오게끔 정리해주는 클래스다.

여러 개의 버튼들이 있지만 그 기능은 유사해서 한 개의 기능을 가지고 여러 기능에 활용할 수 있었다.

메서드들을 새롭게 정의해주는 과정과 그 메서드들이 오버라이드 애너테이션을 통해 한 번 체크되고 구현되는 모습을 볼 수 있다.


    public void paint(Graphics g){
        screenImage = createImage(Main.SCREEN_WIDTH, Main.SCREEN_HEIGHT);
        screenGraphic = screenImage.getGraphics();
        screenDraw((Graphics2D) screenGraphic);
        g.drawImage(screenImage, 0, 0, null);
    }

    public void screenDraw(Graphics2D g) {
        g.drawImage(background, 0, 0, null);
        if(isMainScreen) {
            g.drawImage (selectedImage,250, 50, null);
            g.drawImage (titleImage, 350, 60, null);
        }
        if (isGameScreen) {
            game.screenDraw(g);
        }

        paintComponents(g);
        try {
            Thread.sleep(5);
        } catch (Exception e) {
            e.printStackTrace();
        }
        this.repaint();

 게임 화면의 이미지들이 화면창에 나타나는 과정을 나타낸 메서드들.

마찬가지로 DroptheBeat 클래스 안에 있는 메서드들이며 구체적인 구현과정은 영상을 보면서 '이런 게 있다' 라는 느낌으로 가져갔다.


    public void selectLeft() {
        if(nowSelected == 0)
            nowSelected = trackList.size() - 1;
        else
            nowSelected--;
        selectTrack(nowSelected);
    }

    public void selectRight() {
        if(nowSelected == trackList.size() - 1)
            nowSelected = 0;
        else
            nowSelected++;
        selectTrack(nowSelected);
    }

    public void gameStart(int nowSelected,String difficulty){
        if (selectedMusic != null)
            selectedMusic.close();
        isMainScreen = false;
        leftButton.setVisible(false);
        rightButton.setVisible(false);
        easyButton.setVisible(false);
        hardButton.setVisible(false);
        background = new ImageIcon(Main.class.getResource("../images/" + trackList.get(nowSelected).getGameImage())).getImage();
        backButton.setVisible(true);
        isGameScreen = true;
        game = new Game(trackList.get(nowSelected).getTitleName(), difficulty, trackList.get(nowSelected).getGameMusic());
        game.start();
        setFocusable(true);
    }

    public void backMain() {
        isMainScreen = true;
        leftButton.setVisible(true);
        rightButton.setVisible(true);
        easyButton.setVisible(true);
        hardButton.setVisible(true);
        background = new ImageIcon(Main.class.getResource("../images/mainBackground.jpg")).getImage();
        backButton.setVisible(false);
        selectTrack(nowSelected);
        isGameScreen = false;
        game.close();
    }

    public void enterMain() {
        startButton.setVisible(false);
        quitButton.setVisible(false);
        background = new ImageIcon(Main.class.getResource("../images/mainBackground.jpg")).getImage();
        isMainScreen = true;
        leftButton.setVisible(true);
        rightButton.setVisible(true);
        easyButton.setVisible(true);
        hardButton.setVisible(true);
        intromusic.close();
        selectTrack(0);
    }

메인 화면에 들어갔을 때와, 게임시작, 이후 다시 메인 화면으로 되돌아가는 과정 그리고 각 왼쪽,오른쪽 버튼들이 선택되었을 때 시행되는 기능들을 구현한 메서드들.


package Drop_the_Beat14;

import javazoom.jl.player.Player;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;

public class Music extends Thread {

    private Player player;
    private boolean isLoop;
    private File file;
    private FileInputStream fis;
    private BufferedInputStream bis;

    public Music(String name, boolean isLoop) {
        try{
            this.isLoop = isLoop;
            file = new File(Main.class.getResource("../music/"+name).toURI());
            fis = new FileInputStream(file);
            bis = new BufferedInputStream(fis);
            player = new Player(bis);
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }

    public int getTime() {
        if (player == null)
            return 0;
        return player.getPosition();
    }
    public void close() {
        isLoop = false;
        player.close();
        this.interrupt();
    }
    @Override
    public void run () {
        try {
            do {
                player.play();
                fis = new FileInputStream(file);
                bis = new BufferedInputStream(fis);
                player = new Player(bis);
            } while (isLoop);

        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }

}

Music 클래스

Thread에 대해서 아직 배우지 않았는데 Music 클래스가 Thread클래스를 상속받는 모습에 눈이 간다.

try catch문으로 예외를 잡아주는 모습도 보이고, do { } while( ) 반복문이 시행되는 모습도 볼 수 있다.

mp3파일을 재생하기 위해서 JLayer라는 외부 라이브러리를 다운받고 임포트 해야하는 과정이 존재했다.


public class Game extends Thread {

    private Image noteRouteLineImage = new ImageIcon(Main.class.getResource("../images/noteRouteLine.png")).getImage();
    private Image judgementLineImage = new ImageIcon(Main.class.getResource("../images/judgementLine.png")).getImage();
    private Image gameInfoImage = new ImageIcon(Main.class.getResource("../images/gameInfo.png")).getImage();
    private Image noteRouteSImage = new ImageIcon(Main.class.getResource("../images/noteRoute.png")).getImage();
    private Image noteRouteDImage = new ImageIcon(Main.class.getResource("../images/noteRoute.png")).getImage();
    private Image noteRouteFImage = new ImageIcon(Main.class.getResource("../images/noteRoute.png")).getImage();
    private Image noteRouteSpace1Image = new ImageIcon(Main.class.getResource("../images/noteRoute.png")).getImage();
    private Image noteRouteSpace2Image = new ImageIcon(Main.class.getResource("../images/noteRoute.png")).getImage();
    private Image noteRouteJImage = new ImageIcon(Main.class.getResource("../images/noteRoute.png")).getImage();
    private Image noteRouteKImage = new ImageIcon(Main.class.getResource("../images/noteRoute.png")).getImage();
    private Image noteRouteLImage = new ImageIcon(Main.class.getResource("../images/noteRoute.png")).getImage();

    private String titleName;
    private String difficulty;
    private String musicTitle;
    private Music gameMusic;

    ArrayList<Note> noteList= new ArrayList<Note>();

Game 클래스.

클래스 내에서만 쓰이는 변수, titleName,difficulty,musicTitle,gameMusic의 접근제어자를 private처리 해준 모습을 볼 수 있다. 또, Game클래스의 생성자를 만들어 각 변수들의 초기화를 용이하게 해주었다.

컬렉션 프레임워크의 ArrayList를 활용하고, <Note>라는 타입 매개변수를 이용해 <Note> 타입의 매개변수만 저장할 수 있는 new ArrayList<Note>를 생성한 모습.


    public void pressSpace() {
        noteRouteSpace1Image = new ImageIcon(Main.class.getResource("../images/noteRoutePressed.png")).getImage();
        noteRouteSpace2Image = new ImageIcon(Main.class.getResource("../images/noteRoutePressed.png")).getImage();
    }
    public void releaseSpace() {
        noteRouteSpace1Image = new ImageIcon(Main.class.getResource("../images/noteRoute.png")).getImage();
        noteRouteSpace2Image = new ImageIcon(Main.class.getResource("../images/noteRoute.png")).getImage();
        new Music("drumBig1.mp3", false).start();
    }


    public void pressJ() {
        noteRouteJImage = new ImageIcon(Main.class.getResource("../images/noteRoutePressed.png")).getImage();
        new Music("drumSmall1.mp3", false).start();
    }
    public void releaseJ() {
        noteRouteJImage = new ImageIcon(Main.class.getResource("../images/noteRoute.png")).getImage();
    }


    public void pressK() {
        noteRouteKImage = new ImageIcon(Main.class.getResource("../images/noteRoutePressed.png")).getImage();
        new Music("drumSmall1.mp3", false).start();
    }
    public void releaseK() {
        noteRouteKImage = new ImageIcon(Main.class.getResource("../images/noteRoute.png")).getImage();
    }


    public void pressL() {
        noteRouteLImage = new ImageIcon(Main.class.getResource("../images/noteRoutePressed.png")).getImage();
        new Music("drumSmall1.mp3", false).start();
    }
    public void releaseL() {
        noteRouteLImage = new ImageIcon(Main.class.getResource("../images/noteRoute.png")).getImage();
    }

각 키보드의 자판을 눌렀을 때와, 뗐을 때 noteRouteImage가 변할 수 있도록 설정해주는 과정


    @Override
    public void run() {
        dropNotes();

    }

    public void close() {
        gameMusic.close();
        this.interrupt();
    }

    public void dropNotes() {
        Beat[] beats = null;
        if(titleName.equals("Light - Walen")) {
            int startTime = 4460 - Main.REACH_TIME * 1000;
            int gap = 125;










            };
        } else if (titleName.equals("Cuba - ASHUTOSH")) {
            int startTime = 1000;
            beats = new Beat[] {
                    new Beat(startTime, "Space"),
            };
            
        } else if (titleName.equals("Beautiful Liar - Markvard & AgusAlvarez")) {
            int startTime = 1000;
            beats = new Beat[]{
                    new Beat(startTime, "Space"),
            };
        }

        int i = 0;
        gameMusic.start();
        while (i < beats.length && !isInterrupted()) {
            boolean dropped = false;
            if (beats[i].getTime() <= gameMusic.getTime()) {
                Note note = new Note(beats[i].getNoteName());
                note.start();
                noteList.add(note);
                i++;
                dropped = true;
            }
            if (!dropped){
                try {
                    Thread.sleep(5);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }

각 곡마다 노트가 떨어지는 패턴들을 구현하 조건문을 사용하여 gameMusic의 길이에 따라 게임을 지속여부를 결정한 다음, gameMusic의 진행상황에 따라 노트의 움직임을 제한하는 기능을 구현한 메서드들이다.


public class Note extends Thread {

    private Image noteBasicImage = new ImageIcon(Main.class.getResource("../images/noteBasic.png")).getImage();
    private int x, y = 580 - (1000 / Main.SLEEP_TIME * Main.NOTE_SPEED) * Main.REACH_TIME;
    private String noteType;

    public Note(String noteType) {
        if(noteType.equals("S")) {
            x = 228;
        } else if (noteType.equals("D")) {
            x = 332;
        } else if (noteType.equals("F")) {
            x = 436;
        } else if (noteType.equals("Space")) {
            x = 540;
        } else if (noteType.equals("J")) {
            x = 744;
        } else if (noteType.equals("K")) {
            x = 848;
        } else if (noteType.equals("L")) {
            x = 952;
        }
        this.noteType = noteType;
    }

    public void screenDraw(Graphics2D g){
        if(!noteType.equals("Space")) {

            g.drawImage(noteBasicImage, x, y, null);

        } else {

            g.drawImage(noteBasicImage, x, y, null);
            g.drawImage(noteBasicImage, x + 100, y, null);
        }
    }

    public void drop() {
        y += Main.NOTE_SPEED;
    }

    @Override
    public void run() {
        try {
            while (true) {
                drop();
                Thread.sleep(Main.SLEEP_TIME);
            }
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }

    }
}

각 Note들의 떨어지는 속도와 위치를 조정해주는 Note클래스.

Thread.sleep이 게임을 개발하면서 자주 나오는 거 같은데 곧 개념적으로만 이해하자면,

Thread.sleep()은 Java 프로그래밍에서 사용되는 메소드로, 현재 실행 중인 스레드를 일정 시간 동안 일시적으로 중지하는 데 사용됩니다. 이 메소드는 주어진 시간(밀리초 단위) 동안 스레드의 실행을 일시 중지합니다.

Thread.sleep() 메소드를 호출하면 스레드는 일시적으로 실행이 중단되고, 일정 시간이 경과한 후에 다시 실행됩니다. 스레드가 중지된 동안 다른 스레드가 실행될 수 있습니다.

Thread.sleep()은 다음과 같은 상황에서 유용하게 사용될 수 있습니다:

시간 지연: 특정 작업을 실행하기 전에 일정 시간 동안 기다려야 할 때 사용될 수 있습니다. 예를 들어, 일정 시간 간격으로 반복적으로 작업을 실행하거나, 다른 스레드에게 일정 시간의 대기 시간을 부여하는 경우에 사용될 수 있습니다.

동기화: 여러 개의 스레드가 공유 자원에 동시에 접근하는 것을 제어하고 조절할 수 있습니다. 스레드 사이의 실행 순서를 제어하기 위해 Thread.sleep()을 사용할 수 있습니다.

타이머 및 스케줄링: 일부 작업을 일정한 시간 간격으로 실행하도록 스케줄링하거나, 특정 시간 후에 작업을 실행하도록 타이머를 설정할 때 Thread.sleep()을 사용할 수 있습니다.

다만, 주의해야 할 점은 Thread.sleep()을 남용하면 정확한 시간 제어가 어렵게 되고, 실행 시간이 정확하지 않을 수 있습니다.
또한, Thread.sleep()은 현재 실행 중인 스레드를 중지시키는 메소드이기 때문에 다른 스레드의 작업에도 영향을 줄 수 있으므로 적절한 상황에서 사용해야 합니다.

와 같이 구글링해서 찾을 수 있었다.

간략하게 정리하자면, 특정한 이유때문에 Thread를 일정기간 중지시키는데 사용되는 메서드.


public class KeyListener extends KeyAdapter {

    @Override
    public void keyPressed(KeyEvent e) {
        if (DropTheBeat.game == null) {
            return;
        }

        if(e.getKeyCode() == KeyEvent.VK_S) {
            DropTheBeat.game.pressS();

        } else if (e.getKeyCode() == KeyEvent.VK_D) {
            DropTheBeat.game.pressD();
            
        } else if (e.getKeyCode() == KeyEvent.VK_F) {
            DropTheBeat.game.pressF();
            
        } else if (e.getKeyCode() == KeyEvent.VK_SPACE) {
            DropTheBeat.game.pressSpace();
            
        } else if (e.getKeyCode() == KeyEvent.VK_J) {
            DropTheBeat.game.pressJ();
            
        } else if (e.getKeyCode() == KeyEvent.VK_K) {
            DropTheBeat.game.pressK();
            
        } else if (e.getKeyCode() == KeyEvent.VK_L) {
            DropTheBeat.game.pressL();
            
        }
    }

    @Override
    public void keyReleased(KeyEvent e) {
        if (DropTheBeat.game == null) {
            return;
        }

        if(e.getKeyCode() == KeyEvent.VK_S) {
            DropTheBeat.game.releaseS();

        } else if (e.getKeyCode() == KeyEvent.VK_D) {
            DropTheBeat.game.releaseD();

        } else if (e.getKeyCode() == KeyEvent.VK_F) {
            DropTheBeat.game.releaseF();

        } else if (e.getKeyCode() == KeyEvent.VK_SPACE) {
            DropTheBeat.game.releaseSpace();

        } else if (e.getKeyCode() == KeyEvent.VK_J) {
            DropTheBeat.game.releaseJ();

        } else if (e.getKeyCode() == KeyEvent.VK_K) {
            DropTheBeat.game.releaseK();

        } else if (e.getKeyCode() == KeyEvent.VK_L) {
            DropTheBeat.game.releaseL();

        }

    }

}

키보드의 키가 눌렸을 때와 떼졌을 때 각각 반응하도록 하는 keyListener클래스.

keyAdapter라는 클래스를 상속받아 작성되었다.


public class Track {

    private String titleImage;
    private String startImage;
    private String gameImage;
    private String startMusic;
    private String gameMusic;
    private String titleName;



    public String getTitleImage() {
        return titleImage;
    }

    public void setTitleImage(String titleImage) {
        this.titleImage = titleImage;
    }

    public String getStartImage() {
        return startImage;
    }

    public void setStartImage(String startImage) {
        this.startImage = startImage;
    }

    public String getGameImage() {
        return gameImage;
    }

    public void setGameImage(String gameImage) {
        this.gameImage = gameImage;
    }

    public String getStartMusic() {
        return startMusic;
    }

    public void setStartMusic(String startMusic) {
        this.startMusic = startMusic;
    }


    public String getGameMusic() {
        return gameMusic;
    }

    public void setGameMusic(String gameMusic) {
        this.gameMusic = gameMusic;
    }

    public String getTitleName() {
        return titleName;
    }

    public void setTitleName(String titleName) {
        this.titleName = titleName;
    }


    public Track(String titleImage, String startImage, String gameImage, String startMusic, String gameMusic, String titleName) {
        this.titleImage = titleImage;
        this.startImage = startImage;
        this.gameImage = gameImage;
        this.startMusic = startMusic;
        this.gameMusic = gameMusic;
        this.titleName = titleName;

    }
}

게임 내 재생되는 노래들을 따로 클래스로 나눈 Track 클래스

마찬가지로 생성자를 통해 초기화해주고 각 노래들이 선택되었을 때 보여지거나 수행되는 메서드들을 모아놓았다.


public class Beat {

    private int time;
    private String noteName;


    public int getTime() {
        return time;
    }

    public void setTime(int time) {
        this.time = time;
    }

    public String getNoteName() {
        return noteName;
    }

    public void setNoteName(String noteName) {
        this.noteName = noteName;
    }

    public Beat(int time, String noteName) {
        this.time = time;
        this.noteName = noteName;
    }
}

 노트들이 떨어지는 순간에 맞춘 비트를 생성하는 Beat클래스


게임 내 이미지들

게임 시작 화면 이미지
플레이할 노래 선택 화면 1
플레이할 노래 선택화면 2
게임 플레이 화면, 키보드를 누르면 noteRoute의 색이 바뀌며, 노트가 들어갔을 때 판정라인에 맞춰 해당 자판을 누르면 점수를 획득하는 기능은 아직 미구현


Java에서 배운 문법들을 살펴본다는 느낌으로 가볍게 시작한 리듬게임 만들기 프로젝트였지만 되게 재밌었고 흥미로웠다.

하루종일 붙들고 사용된 문법들과 기능들을 살펴보는 과정이 마냥 지루하지 않았다. 배웠던 문법들이 실제로 어떻게 사용되는지 살펴볼 수 있는 좋은 과정이었던 것 같다.

 

또, 객체지향설계가 어떤 것인지 보다 직접적으로 느낄 수 있게 된 것 같다. 게임에 사용되는 요소들을 하나의 객체로 묶어 따로 관리해주고 객체들 고유의 캡슐화를 통해 다른 객체가 보내온 메세지들을 내부적으로 선택하여 프로그램이 돌아가는 과정은 하나의 유기체를 보는 듯한 모습이었다. 

 

클론 코딩의 과정 중 어려웠던 점은 강의는 이클립스라는 통합개발환경으로 진행되는 반면, 나는 인텔리제이를 쓰고 있었기에 둘의 차이를 인지하면서 서로 다른 단축키를 구글링하면서 찾아야했던 과정이다. 그리고 곡의 BPM에 맞게 비트를 작성해 노트가 떨어지는 순간을 직접 작업해야한다는 점이었다. 음악적 소질이 전무한 나였기에 직접 노래를 들으면서 비트를 맞춰보고 있고 이 과정은 현재진행중이다. 즉 아직은 게임이 미완성이지만 차차 비트를 작성하여 완성시킬 생각이다. 또, 디자인적 센스나, 이미지, 노래에 대해서 non Copyright된 컨텐츠를 찾는 과정도 시간이 많이 들었다. 라이센스를 확인하면서 노래들을 선별하였고, 무료 이미지 사이트에서 사진, 버튼 이미지들을 충당할 수 있었다. 디자인적 센스는 내 역량이라 많이 아쉽게 다가왔지만 그보다 무언가 프로그래밍을 하는 것에 초점을 맞추어 진행하였다.

 

새로 느꼈던 점은 생각보다 포토샵,이미지 관리가 중요하게 다가왔다는 점이다. 물론 1인 개발에 주제가 게임이라 그런 면이 없지 않겠지만, 이번 기회에 처음으로 포토샵을 접해본 나로써는 상당히 어색하고 낯설었다. 다행히 큰 무리없이 적응하여 적당히 모양은 맞출 수 있었다.

 

반나절을 쏟아가면 만든 첫 프로그램이다. 아직 미완이지만 시간이 날때마다 틈틈이 완성시켜보고 테스트할 생각이다. 클론코딩으로 만들었지만, 책으로만 배우고 예제로만 풀어본 문법적 개념들을 실제로 사용되는 모습들을 보며 짚고 넘어갈 수 있는 부분이 좋았다. 다음부터 클론코딩, 혹은 프로젝트를 진행하면서 각 코드들이 어떤 기능 혹은 내용을 담고 있는지에 대해 주석을 달아야겠다.