국비교육(22-23)

73일차(2)/Android App(23) : View 로 미니 슈팅게임 만들기(2)

서리/Seori 2023. 1. 19. 20:44

73일차(2)/Android App(23) : View 로 미니 슈팅게임 만들기(2)

 

 

GameView

package com.example.step09gameview;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class GameView extends View {
    //필드
    Bitmap backImg; //배경 이미지
    int width, height; //화면의 폭과 높이(GameView 가 차지하고 있는 화면의 폭과 높이)

    //드래곤의 이미지를 저장할 배열
    Bitmap[] dragonImgs = new Bitmap[4];
    //드래곤 이미지 인덱스
    int dragonIndex=0;
    //유닛(드래곤,적기)의 크기를 저장할 필드
    int unitSize;
    //드래곤의 좌표를 저장할 필드(가운데 기준)
    int dragonX, dragonY;
    //배경이미지의 y좌표
    int back1Y, back2Y;
    //카운트를 셀 필드
    int count;
    //적기 이미지를 저장할 배열
    Bitmap[][] enemyImgs=new Bitmap[2][2];
    //Enemy 객체를 저장할 List
    List<Enemy> enemyList=new ArrayList<>();
    //적기의 x 좌표를 저장할 배열
    int[] enemyX=new int[5];
    //랜덤한 숫자를 얻어낼 Random 객체
    Random ran=new Random();

    //Missile 객체를 저장할 리스트
    List<Missile> missList=new ArrayList<>();
    //미사일의 크기
    int missSize;
    //미사일 이미지를 담을 배열
    Bitmap[] missImgs=new Bitmap[3];
    //미사일의 속도
    int speedMissile;

    public GameView(Context context) {
        super(context);
    }

    public GameView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    //초기화 메소드
    public void init(){
        //원본 배경 이미지 읽어들이기
        Bitmap backImg= BitmapFactory.decodeResource(getResources(), R.drawable.backbg);
        //배경이미지를 view 의 크기에 맞게 조절해서 필드에 저장
        this.backImg=Bitmap.createScaledBitmap(backImg, width, height, false);
        //드래곤 이미지를 로딩해서 사이즈를 조절하고 배열에 저장한다.
        Bitmap dragonImg1=
                BitmapFactory.decodeResource(getResources(), R.drawable.unit1);
        Bitmap dragonImg2=
                BitmapFactory.decodeResource(getResources(), R.drawable.unit2);
        Bitmap dragonImg3=
                BitmapFactory.decodeResource(getResources(), R.drawable.unit3);
        dragonImg1=Bitmap
                .createScaledBitmap(dragonImg1, unitSize, unitSize, false);
        dragonImg2=Bitmap
                .createScaledBitmap(dragonImg2, unitSize, unitSize, false);
        dragonImg3=Bitmap
                .createScaledBitmap(dragonImg3, unitSize, unitSize, false);
        dragonImgs[0]=dragonImg1;
        dragonImgs[1]=dragonImg2;
        dragonImgs[2]=dragonImg3;
        dragonImgs[3]=dragonImg2;

        //미사일 이미지 로딩
        Bitmap missImg1=BitmapFactory.decodeResource(getResources(),
                R.drawable.mi1);
        Bitmap missImg2=BitmapFactory.decodeResource(getResources(),
                R.drawable.mi2);
        Bitmap missImg3=BitmapFactory.decodeResource(getResources(),
                R.drawable.mi3);
        //미사일 이미지 크기 조절
        missImg1=Bitmap.createScaledBitmap(missImg1,
                missSize, missSize, false);
        missImg2=Bitmap.createScaledBitmap(missImg2,
                missSize, missSize, false);
        missImg3=Bitmap.createScaledBitmap(missImg3,
                missSize, missSize, false);
        //미사일 이미지를 배열에 넣어두기
        missImgs[0]=missImg1;
        missImgs[1]=missImg2;
        missImgs[2]=missImg3;

        //적기 이미지 로딩
        Bitmap enemyImg1=BitmapFactory
                .decodeResource(getResources(), R.drawable.silver1);
        Bitmap enemyImg2=BitmapFactory
                .decodeResource(getResources(), R.drawable.silver2);
        Bitmap enemyImg3=BitmapFactory
                .decodeResource(getResources(), R.drawable.gold1);
        Bitmap enemyImg4=BitmapFactory
                .decodeResource(getResources(), R.drawable.gold2);

        //적기 이미지 사이즈 조절
        enemyImg1=Bitmap.createScaledBitmap(enemyImg1,
                unitSize, unitSize, false);
        enemyImg2=Bitmap.createScaledBitmap(enemyImg2,
                unitSize, unitSize, false);
        enemyImg3=Bitmap.createScaledBitmap(enemyImg3,
                unitSize, unitSize, false);
        enemyImg4=Bitmap.createScaledBitmap(enemyImg4,
                unitSize, unitSize, false);
        //적기 이미지 배열에 저장
        enemyImgs[0][0]=enemyImg1; //0행 0열 silver1
        enemyImgs[0][1]=enemyImg2; //0행 1열 silver2
        enemyImgs[1][0]=enemyImg3; //1행 0열 gold1
        enemyImgs[1][1]=enemyImg4; //1행 1열 gold2
        //적기의 x좌표를 구해서 배열에 저장한다.
        for(int i=0; i<5; i++){
            enemyX[i]= i *unitSize+ unitSize/2;
        }



        //handler 객체에 메세지를 보내서 화면이 주기적으로 갱신되는 구조로 바꾼다.
        handler.sendEmptyMessageDelayed(0,20);
    }

    //View가 활성화될 때 최초 한번 호출되고 View의 사이즈가 바뀌면 다시 호출된다.
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        //view가 차지하고 있는 폭과 높이가 px 단위로 w, h에 전달된다.
        width=w;
        height=h;
        //unitSize는 화면 폭의 1/5로 설정
        unitSize=w/5;
        //드래곤의 초기 좌표 부여
        dragonX=w/2;
        dragonY=height-unitSize/2;

        //배경이미지의 초기 좌표
        back1Y=0;
        back2Y=-height;

        //미사일의 크기는 드래곤의 크기의 1/4
        missSize=unitSize/4;

        //미사일의 속도는 화면의 높이/50
        speedMissile=h/50;

        //초기화 메소드 호출
        init();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        // (0,0) 좌표에 배경 이미지 그리기
        canvas.drawBitmap(backImg, 0, back1Y, null);
        canvas.drawBitmap(backImg, 0, back2Y, null);

        //미사일 그리기
        for(Missile tmp:missList){
            canvas.drawBitmap(missImgs[0], tmp.x-missSize/2, tmp.y-missSize/2, null);
        }
        //적기 그리기
        for(Enemy tmp:enemyList){
            canvas.drawBitmap(enemyImgs[tmp.type][tmp.imageIndex], tmp.x-unitSize/2, tmp.y-unitSize/2, null);
        }

        //드래곤 그리기
        canvas.drawBitmap(dragonImgs[dragonIndex], dragonX-unitSize/2, dragonY-unitSize/2, null);

        //----- 배경이미지 관련 처리-----
        back1Y += 5;
        back2Y += 5;

        //만일 배경 1의 좌표가 아래로 벗어나면
        if(back1Y>=height){
            //배경1을 상단으로 다시 보낸다.
            back1Y=-height;
            //배경2와 오차가 생기지 않게 하기위해 복원하기
            back2Y=0;
        }
        //만일 배경2의 좌표가 아래로 벗어나면
        if(back2Y>= height){
            //배경2을 상단으로 다시 보낸다.
            back2Y=-height;
            //배경1과 오차가 생기지 않게 하기위해 복원하기
            back1Y=0;
        }

        count++;

        //----- 드래곤 애니메이션 관련 처리 -----
        if(count%10==0){
            dragonIndex++;
            if(dragonIndex==4){ //만일 존재하지 않는 인덱스라면
                dragonIndex=0; //다시 처음으로 되돌리기
            }
        }

        missileService(); //미사일 관련 처리 메소드 호출
        enemyService(); //적기 관련 처리 메소드 호출
    }

    //미사일 관련 처리
    public void missileService(){
        //미사일 만들기
        //테스트로 미사일 객체 한개를 배열에 넣어두기
        if(count%5==0){
            missList.add(new Missile(dragonX, dragonY));
        }

        //미사일 움직이기
        for(Missile tmp:missList){
            tmp.y -= speedMissile;
            //만일 위쪽 화면을 벗어났다면
            if(tmp.y < -missSize/2){
                tmp.isDead=true; //배열에서 제거될수 있도록 표시해둔다.
            }
        }
        //미사일 객체를 모두 체크해서 배열에서 제거할 객체는 제거하기(단 반복문을 역순으로 돌아야 한다)
        for(int i=missList.size()-1; i>=0; i--){
            //i번째 미사일 객체를 얻어와서
            Missile tmp=missList.get(i);
            //만일 제거할 미사일 객체라면
            if(tmp.isDead){
                //List에서 i번째 아이템을 제거한다.
                missList.remove(i);
            }
        }
    }

    //적기 관련 처리
    public void enemyService(){

        if(count%10 == 0){
            //반복문 돌면서
            for(Enemy tmp:enemyList){
                //모든 적기의 이미지 인덱스를 i 증가시킨다.
                tmp.imageIndex++;
                if(tmp.imageIndex==2){ //만일 존재하지 않는 인덱스라면
                    //다시 처음으로 돌리기
                    tmp.imageIndex=0;
                }
            }
        }

        //임의의 시점에 적기가 5개 만들어지도록 해보세요.
        int ranNum=ran.nextInt(20);
        if(ranNum==10){
            //임의의 시점에 적기가 5개 만들어지도록 해보세요
            for(int i=0;i<5;i++){
                Enemy tmp=new Enemy();
                tmp.x=enemyX[i]; //x좌표는 배열에 미리 준비된 x좌표
                tmp.y=unitSize/2; //임시 y좌표
                tmp.type=ran.nextInt(2); //적기의 타입은 0 또는 1로 랜덤하게 부여
                tmp.energy= tmp.type == 0 ? 50 : 100;
                //만든 적기를 적기 목록에 담기
                enemyList.add(tmp);
            }

        }

        //적기 움직이기
        for(Enemy tmp:enemyList){
           //적기의 y좌표를 증가시키고
           tmp.y += speedMissile/2;
           //아래쪽으로 화면을 벗어났다면
           if(tmp.y > height+unitSize/2){
               //배열에서 제거될 수 있도록 표시한다.
               tmp.isDead=true;
           }
       }
       //적기 체크해서 배열에서 삭제할 적기는 제거하기
       for(int i=enemyList.size()-1;i>=0;i--){
           Enemy tmp=enemyList.get(i);
            if(tmp.isDead){
                enemyList.remove(i);
            }
       }


    }

    //view에 터치 이벤트가 발생하면 호출되는 메소드
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        dragonX=(int)event.getX();
        return true;
    }

    Handler handler=new Handler(){
        @Override
        public void handleMessage(@NonNull Message msg) {
            //화면 갱신하기
            invalidate();
            //handler객체에 빈 메세지를 10/1000 초 이후에 다시 보내기
            handler.sendEmptyMessageDelayed(0, 20);
        }
    };
}

 

MainActivity

 

- setContentView() 가 오버로딩 되어있다.

- 주로 2번을 많이 사용했는데(정수 전달), 이건 뷰를 전달했으므로 1번째로!

- 위에서 생성한 GameView 객체를 받을 것이다. 받아서 화면을 구성한 것

 

 

- 게임뷰를 생성할 수 있는 생성자 2개

- 레이아웃xml 문서를 사용해서 화면 구성 가능

 

- 어제 한 것: 배경이미지 무한스크롤, 유닛을 터치입력에 반응시켜서 좌우로 움직일 수 있게 함

 

- init() 메소드는 onSizeChanged() 에서 호출된다.

- 이 onSizeChanged 는 뷰가 처음 화면에 구성될 때 호출된다.

 

- Handler에 메시지를 호출하면서 빈 메세지를 전달

- what에는 메세지의 종류(지금은 중요하지않다), delayMillis에는 얼마의 시간을 지연해서 보낼지 전달!

 

- 메시지를 보내면? handler 메소드가 실행된다. 화면이 갱신되고, 빈 메세지를 또 보낸다.

- 이렇게 반복적으로 화면이 새로 호출되도록 만드는 것.

 

- 현재 초당 100번 호출되도록 되어있다.

- 즉 onDraw 메소드가 초당 100번 호출되는것과 같다. 화면을 1초에 100번 다시 그린다!

- 초당 100번 화면이 갱신되면 우리에게는 움직이는 것처럼 보인다.

 

- 터치하면 그 터치의 x좌표를 받아서 드래곤의 x좌표에 반영한다.

 


 

- 드래곤의 이미지를 바꾸려면?

- 드래곤 이미지 배열에 있는 0~3번째 방을 참조하기!

 

- 이렇게 if문으로 0~3번 방을 돌도록 작성해 주었는데, 실행해보면 너무 빨리 움직인다.

- 현재 1초에 몇십번씩 갱신되므로 빈도수를 좀 다르게 해야 한다.

 

if(count%10==0){
    dragonIndex++;
    if(dragonIndex==4){
        dragonIndex=0; 
    }
}

 

- Count 필드 선언하고, 화면을 그릴때마다 count++; 되도록 하기

- 10으로 나눈 나머지가 0일때만 실행하고 싶다면 만들었던 dragonIndex 블럭을 if문 안으로 넣어주기만 하면 된다.

 


 

- 미사일의 초기좌표는 드래곤의 위치에서 시작되어야 한다.

- 시간이 지날수록 Y좌표가 줄어들어야 한다.

- 각각의 미사일을 배열로 관리하고, 고유한 위치가 있어야 한다.

 

- 미사일은 여러개, 미사일 하나하나를 관리할 객체를 생성해야 한다.

- 새 클래스 생성!

 

 

Missile 클래스

package com.example.step09gameview;

public class Missile {
    public int x;
    public int y;
    public boolean isDead=false;

    //생성자
    public Missile(int x, int y){
        this.x=x;
        this.y=y;
    }
}

 

- xy좌표가 있어야하고, 미사일 객체 하나하나의 xy 좌표가 따로 관리되어야 한다.

 

- 미사일 객체는 많으므로 리스트에 넣어서 관리

- 크기는 계산해서 얻어내기

- 3개의 비트맵 이미지를 배열에 저장

 

- 미사일 이미지 3개를 로딩해서 -> 크기를 조절하고 -> 배열에 미리 넣어 준비해두는 작업!

- 필요할때 이 이미지를 참조해서 그리면 된다.

 

- 미사일 객체 안의 필드를 읽어서 x,y값으로 사용

- 정확한 좌표는? 폭의 반, 높이의 반만큼을 빼주어야 한다.

- 미사일은 여러개이므로 for문으로 반복문에 넣어주기

 

- 하지만 missList는 지금 빈 배열이다. 배열에 이미지를 넣어주기!

 

- 드래곤의 x,y좌표에서 시작하는 미사일객체를 하나 만들어본다.

 

 

- 지금은 움직이지 않는다. 이 미사일을 위로 움직이게 하려면 미사일 객체의 y좌표를 감소시켜야 한다!

- 반복문 돌면서 (미사일 객체는 여러개가 될 것이므로) 미사일 객체를 참조해서 y좌표를 줄여준다.

 

- 이렇게 미사일이 점점 앞으로 나아가게 된다.

 

- 미사일의 속도는? speedMissile 필드생성

- 미사일 속도는 화면의 높이의 1/50으로 설정

- 이렇게 비율로 써야만 디바이스에 따라 조건이 달라지는 것을 방지할 수 있다.

 

- y좌표의 값을 줄여주기. 이렇게 미사일의 속도에 따라 y값이 줄어들게 된다.

 


 

- 미사일 개수조절

 

- 현재 상태로는 미사일이 너무 많이 만들어진다.(count로 개수 조절)

- 그리고 화면을 벗어난 미사일은 더이상 관리되지 않는 처리도 해야 한다!

 

- count를 사용해서 1/5로 줄이면 이 정도로 보이게 된다.

 

 

- 미사일 제거하기 기능의 적용 시점

1) 화면을 벗어나면 제거한다.

2) 적기를 만났을 때 제거한다.

 

- 제거해야 할 지 말아야 할 지 여부를 미사일 객체가 자체적으로 가지도록 만들 것이다. (필드에 넣기!!!!)

 

- 미사일 클래스에 새 필드 추가! isDead

- 미사일 객체의 높이의 반이 y좌표보다 작아졌을 때(-missSize/2) isDead=true 로 바꾸면 된다.

 

- missList 에 있는 미사일이 화면을 벗어난다면, 반복문 돌면서 제거하기

 

 

- 지금은 isDead를 true로 만드는 조건이 이거 하나밖에 없지만,

 나중에는 적기를 만날 때에도 true가 되고 missList에서 제거되도록 할 것!

 


 

- 미사일 메소드 분리

- 이 메소드(미사일 관련 처리)를 별도로 분리하려고 한다.

 

- 아래 만든 missileService 메소드로 옮기고, 이 메소드를 onDraw에서 호출하는 구조로 바꾸기

 

- 메소드 위치 조정하기. 미사일을 나중에 그리면 드래곤의 등에서 나오게 된다.

- 미사일 그리기를 드래곤 그리기보다 먼저 해야 드래곤이 위에 그려지게 된다.

 


 

- 적기 관련 처리를 위한 필드추가!

Bitmap[][] enemyImgs=new Bitmap[2][2];

 

- 적기 이미지를 저장할 배열. 2차원 배열이다.

- 적기도 날개가 펄럭거리게 하기 위해, 총 4개의 적기 이미지를 2차원 배열에 담을 것이다.

 

 

- 0행 0열, 0행 1열 로 만들어진 배열이다.(행렬이라고 생각하면 된다.)

- s1, s2 / g1, g2 이미지를 배열로 관리할 것이다.

 

- size는 unitsize로 하고, 적기 이미지 배열에 저장

- 0-0, 0-1에 silver 적기 이미지 / 1-0, 1-1열에 gold 적기 이미지를 넣어둔 것이다.

 

- 2차원 배열을 생성하고 해당 방을 참조하는 방법 기억하기! [행번호][열번호] 를 넣으면 된다.

 


 

- 적기의 등장방법

 

 

- 위쪽에서 5개가 1세트로 만들어져서 아래쪽으로 진행하도록 할 것이다.

- 랜덤한 시점에 랜덤 적기가 한번에 5개씩 나오고, y좌표가 늘어난다.

- 적기의 에너지를 다르게 부여한다. 적기 하나하나도 고유한 정보를 가지고 있어야 한다.

- 이 개별 객체를 관리해줄 수 있어야 함!(새 클래스로)

 

 

Enemy

package com.example.step09gameview;

public class Enemy {
    public int x,y; //적기의 좌표
    public int type; //적기의 type 0 or 1
    public boolean isDead; //배열에서 제거할지 여부
    public int energy; //에너지
    public int imageIndex; //적기의 이미지 인덱스(애니메이션 효과를 주기위해)

}

 

- 좌표, 적기 타입, isDead, 에너지 필드 생성

 

- 적기 좌표를 미리 계산해서 enemyX에 그려놓고 그자리에 나타나게 할 것이다.

 

- 적기의 x좌표를 구해서 배열에 저장하기.적기의 좌표는 5개 중 하나이다!!

- 적기의 x좌표를 이렇게 그린다.(화면 폭의 1/5 사이즈)

  1은 unitSize의 1/2,

  2는 1*unitSize + unitSize1/2 을 더한 값,

  2는 2*unitSize + unitSize1/2 을 더한 값,  ...

- 이렇게 계산하면 x좌표 5개를 순서대로 구할 수 있다.

 


 

- onDraw에 적기 그리기

 

 

- 일단 0번 타입의 적만 그린다고 가정한다.(이미지가 움직이지 않음)

- 적기는 고유한 x,y좌표가 있을 것이므로(tmp.x, tmp.y) 해당 값을 가져와서 그려주기

 

 

- 먼저 적기 5개를 test로 만들어보기.

- 3항연산자로 에너지는 50 또는 100을 부여한다.(은색이 50, 금색이 100)

 

 

- enemyX를 순서대로 불러올 수 있도록 i로 반복문을 돌아서 출력한다.

- 두 종류의 적 중 랜덤으로 5개를 가져온다.

 

- 화면에 이렇게 나타난다.

 


 

- 이 적기의 y좌표는 아직 움직이지 않는다. 초기값을 부여한 이후에 변하지 않아서!

 

for(Enemy tmp:enemyList){  
	tmp.y += speedMissile/2;
}

 

- EnemyService 메소드를 생성해서 안에서 처리한다!

- 적기가 여러 개이므로 for문 돌면서 이동하기

- 미사일의 속도만큼 y좌표의 값을+ 증가시키기! 그러면 적기가 아래로 내려온다.

 

 

- 반복문을 역순으로 돌면서 i번째 인덱스를 가져와서 tmp.isDead가 true인 적기를 list에서 제거하기

 역순으로 도는 이유는? 앞에서부터 제거하면 그 다음 인덱스가 자동으로 한칸씩 앞으로 온다.

- 그래서 뒤쪽 인덱스에서부터 체크해서 제거한다.

 

- 이런 식으로 종종 배열을 순회할때 맨 뒤에서부터 순회해야 할 때가 있다.

 


 

- 적기 반복 생성 / 생성 빈도 조절

 

- 적기 메소드에 적기가 생성되는 for문을 enemyService 에다가 옮겨주었더니

 매 화면이 갱신될때마다 매번 만들어지게 된다.

 

- 1~19 사이의 랜덤한 정수를 얻어내서 우연히 10이 나오면 만든다.

- 그러면 약 1/20 확률로 적기가 생성되게 된다.

 


 

- 적기 관련해서 기능을 개선해보기

- enemy의 필드 추가

 

- 화면에 적기를 그릴때 0만 그리는 것이 아니라 이미지 2개 중 바뀌도록 하기(날개가 파닥이는 효과)

- 너무 자주 바뀌지않고 적당한 주기로!

 

if(count%20 == 0){
    for(Enemy tmp:enemyList){
        tmp.imageIndex++;
        if(tmp.imageIndex==2){
            tmp.imageIndex=0;
        }
    }
}

- if문 count 에 일정한 숫자를 넣어서 날개가 움직이게 한다.

 

- 너무 자주 바뀌지 않도록 적절한 수를 넣어주면 된다.