국비교육(22-23)

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

서리/Seori 2023. 1. 19. 23:25

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

 

 

GameView

package com.example.step09gameview;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
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;


    //Missile 객체를 저장할 리스트
    List<Missile> missList=new ArrayList<>();
    //미사일의 크기
    int missSize;
    //미사일 이미지를 담을 배열
    Bitmap[] missImgs=new Bitmap[3];
    //미사일의 속도
    int speedMissile;
    //적기 이미지를 저장할 배열
    Bitmap[][] enemyImgs=new Bitmap[2][2];
    //Enemy 객체를 저장할 List
    List<Enemy> enemyList=new ArrayList<>();
    //적기의 x 좌표를 저장할 배열
    int[] enemyX=new int[5];
    //랜덤한 숫자를 얻어낼 Random 객체
    Random ran=new Random();
    //적기가 만들어진 이후 count를 셀 필드
    int postCount;
    //점수 필드
    int point;

    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) 좌표에 배경 이미지 2개 그리기
        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){

            if(tmp.isFall){ //추락 상태인 적기
                //canvas의 정상 상태(변화를 가하지 않은 상태)를 저장
                canvas.save();
                //적기의 위치로 평행이동
                canvas.translate(tmp.x, tmp.y);
                canvas.rotate(tmp.angle);
                //좀더 줄어든 크기의 Bitmap 이미지를 얻어내서
                Bitmap scaled=Bitmap.createScaledBitmap(enemyImgs[tmp.type][tmp.imageIndex],
                        tmp.size, tmp.size, false);
                //적기를 원점에 그린다.
                canvas.drawBitmap(scaled, 50-unitSize/2, -unitSize/2, null);
                //저장했던 정상 상태도 되돌린다.
                canvas.restore();
            }else{ //정상 상태의 적기
                canvas.drawBitmap(enemyImgs[tmp.type][tmp.imageIndex], tmp.x-unitSize/2, tmp.y-unitSize/2, null);
            }
        }

        //글자를 출력하기 위한 paint 객체
        Paint textP=new Paint();
        textP.setColor(Color.YELLOW);
        textP.setTextSize(50);
        //점수 출력하기. drawText( 출력할 문자열, 좌하단의 x, 좌하단의 y, Paint 객체 )
        canvas.drawText("Point : "+point, 10, 60, textP);

        //드래곤 그리기
        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(); //적기 관련 처리 메소드 호출
        checkStrike(); //적기와 미사일의 충돌 검사 메소드 호출
    }

    //적기와 미사일의 충돌 검사하기
    public void checkStrike(){
        for(int i=0;i<missList.size();i++){
            //i번쨰 미사일 객체
            Missile m=missList.get(i);
            for(int j=0; j<enemyList.size(); j++){
                //j번째 적기 객체
                Enemy e=enemyList.get(j);
                //i번째 미사일이 j번째 적기의 4각형 영역 안에 있는지 여부
                boolean isStrike= m.x > e.x - unitSize/2 &&
                                  m.x < e.x + unitSize/2 &&
                                  m.y > e.y - unitSize/2 &&
                                  m.y < e.y + unitSize/2;
                if(isStrike && !e.isFall){//현재 추락중인 적기는 무시하기
                    //적기 에너지를 줄이고
                    e.energy -= 50;
                    //미사일을 없앤다.
                    m.isDead=true;
                    //만일 적기의 에너지가 0 이하이면 적기가 제거되도록 한다.
                    if(e.energy <= 0){
                        //e.isDead=true; //적기는 완전히 추락한 이후에 제거되게 한다.
                        e.isFall=true; //바로 제거되는 대신 적기가 추락 상태가 되도록 한다.
                        //점수 올리기
                        point += e.type == 0 ? 100 : 200;
                    }
                }
            }
        }
    }

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

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

        postCount++;
        int ranNum=ran.nextInt(20);
        if(ranNum==10 && postCount > 13){
            postCount=0;
            //임의의 시점에 적기가 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;
                tmp.size=unitSize; //적기의 초기 크기
                //만든 적기를 적기 목록에 담기
                enemyList.add(tmp);
            }

        }

        //적기 움직이기
        for(Enemy tmp:enemyList){
            if(tmp.isFall) {
                //크기를 줄이고
                tmp.size -= 1;
                //회전값을 증가 시킨다.
                tmp.angle += 10;
                //만일 크기가 0보다 작아진다면
                if (tmp.size <= 0) {
                    //배열에서 제거될 수 있도록 표시한다.
                    tmp.isDead = true;
                }
            }

           //적기의 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);
            }
       }
    }

    //미사일 관련 처리
    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);
            }
        }
    }

    //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객체에 빈 메세지를 20/1000 초 이후에 다시 보내기
            handler.sendEmptyMessageDelayed(0, 20);
        }
    };
}

 

 

- 미사일 충돌 검사

 

- 미사일의 x,y좌표가 적기의 x,y좌표안에 들어갔을 때 적기가 죽었다고 판정할 예정!

 

- 충돌하면(적기와 미사일의 위치가 같아지면)

  미사일과 같은 에너지가 닳고, 에너지가 0이 되면 죽는다.

 

- 그리고 미사일과 적기의 충돌 검사를 할 예정인데

 미사일도 여러개, 적기도 여러개이므로 for문 안에 for문이 있어야한다.

 

- 새 메소드 생성 (checkStrike)

- 구구단 출력방식도 이렇게 다중for문을 사용한다.

- 배열 2개를 각각 1:1로 대응시켜 검사하려면 for문 안에 for문이 있는 구조여야 한다.

 

- 적기가 이렇게 있으면 e.x, e.y 좌표에 적기가 있다고 보면 된다.

 

- 분홍색 영역 안쪽 위치가 미사일이 들어가야하는 좌표 (충돌한다고 하면 들어가야 하는 위치!)

- 폭의 반만큼을 뺀 것보다는 커야하고, 더한것보다는 작아야하고,

 높이의 반을 뺀 것보다는 커야하고 더한것보다는 작아야 한다.

 

- 사각형 안에 들어가는가 여부를 이렇게 && 으로 함께 표시해준다.

 

- 부딪쳤을 때의 로직을 if문으로 작성해주기

 

- 이제 미사일에 맞은 적기가 사라진다.

 

- 이 충돌 검사까지 할 수 있으면 슈팅게임의 로직은 어느정도 완성!

 


 

- 랜덤이지만, 너무 자주 생산되어 적기끼리 겹치는 문제가 생긴다.

- 만약 적기끼리 겹쳐지는 것을 막으려면

 일정 카운트가 진행되었는지, 또는 일정 시간이 흘렀는지를 파악해서 적기를 생성하는 구조로 만들면 된다.

 

postCount 필드 생성

 

- postCount 가 얼마이상 되었을때 적기를 생성하게 하려면

 기존 if문에 postCount 값을 추가조넣으로 넣어주면 된다.

 

- 게임을 더 어렵게 만들려면 금색 적기의 에너지를 높게 하거나

  임의의 시점에 운석이 확 떨어지게 만들어볼 수 있다.

 


 

- 점수 카운트하기

 

- 점수를 계산할 point 필드 추가

 

- drawText() 라는 메소드가 있다. 화면 위에 텍스트를 그려준다!

 

- 글자의 좌표는 좌하단 기준이다.

 

- 이렇게 지정한 위치에 글자가 출력된다.

 

- 점수를 올리는 로직은?

- 적기의 에너지가 0 이하일 때, 이 지점에서 적용되어야한다.

 

- 타입이 0, 1 두 종류 있으므로,

 0을 죽이면 100점, 1을 죽이면 200점으로 한다.

 


 

- 적기가 죽으면 바닥으로 추락하는 애니메이션 넣어보기!

- 이미지를 제자리에서 회전하면서 크기가 점점 작아지게 한다.

 

- 이전과 조금 다른 방법으로 그려보려고 함!

 

- View 위에 canvas가 얹혀져 있는 형태이다.

- canvas에서 작업하면 View의 위치에 찍힌다. 도화지 위에 먹지가 있다고 생각하기.

- 좌표계가 설정되어 있다.

 

- 이 좌표계를 원한다면 translate 할 수 있다. 좌표계의 평행이동 가능!

- 캔버스에서 평행이동한 상태로 그린다면 다른 위치에 어떤 개체가 찍힐 수 있다.

 

- 이 좌표계는 평행이동도 가능하고 회전도 가능하다.

 

canvas.rotate(30);

- rotate()로 캔버스를 30도 회전시킨 것이다.

 

canvas.translate(100,100);
canvas.rotate(30);

 

- 평행이동도 시키고 회전도 한 상태이다!

 

 

- 적기를 회전시키려면 각도를 먼저 회전시키고 나서 원점에 적기를 그린다고 생각하기.

- 적기의 위치로 평행이동한다.

- 이 좌표는 -unitSize

- 각도를 넣어서 원하는만큼 rotate 가능하다.

 

- 기본 상태를 저장해놓고, 적기를 그린 이후에는 다시 원상태로 되돌린다.

- 적기를 그릴때만 캔버스를 변화시킨다.

 

- for 문 안에서 저장해주기!

 

 

- 평행이동시켜서 돌려보아도 똑같다. 0,0 이다.

 

- 이렇게 각도를 정하면 적기가 기울어져서 그려져서 내려온다.

- 적기가 추락하게 하기 위한 새로운 필드가 필요하다.

 

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; //적기의 이미지 인덱스(애니메이션 효과를 주기위해)
    public boolean isFall; //현재 추락하고 있는지 여부
    public int angle; //현재 회전각
    public int size; //현재 크기

}

 

- 적기에 isFall, angle, size 필드 추가!

- 회전하면서 크기를 줄일 것이다.

 

 

- 적기의 크기를 추가해준다.

- 지금은 미사일에 맞으면 바로 제거하는데, 떨어지면서 작아지도록 할 예정

 

- 적기를 그릴 때 true냐 아니냐에 따라 적기를 다르게 그릴 것이다!

 

- if문으로 나누어서 추락 상태일때는 아래 블럭으로 복잡하게 그리고

 else일 때는 정상상태로 그리면 된다.

 

- 이렇게 다르게 그려준다.

 

- 또한 적기의 줄어든 이미지를 얻어내서, 이 줄어든 이미지를 그린다.(scale)

- 각도를 변화시키면서 작아진 이미지를 그려주기

 

- 적기 그리기뿐만 아니라 움직이기도 if로 분기한다.

- 추락중이 아니면 그대로 하고, 추락중이면 다른 움직임을 넣어주기

 

- 추락중인 적기는 크기를 1씩 줄이고, 회전값을 10씩 증가시키고,

 크기가 0보다 작아지면 배열에서 제거한다.

 

- 추락중이 아니면 y좌표만 증가시키기

 


 

- 적기가 회전하면서 떨어지는데, 문제가 있다.

- 떨어지는 적기도 아직 살아있는것으로 인지되어 미사일에 맞는다.

 

- isStrike 메소드를 수정해주면 된다.

- 부딪친다고 판단하는 조건에 추락중이 아니라는 조건(!e.isFall)을 추가로 부여

- 현재 추락중인 적기는 무시하기. 추락중인 적기가 미사일을 맞지 않도록!

 

- 이제 이렇게 나온다. 작아지고 회전하면서 떨어진다.

 

- 추락할때는 원점이 아니라 약간 다른위치에 그려주면

 그리는 지점이 변화되어 회전을 더 넓게 해서 떨어지는 실감나는 애니메이션이 된다.

 


 

- 게임 첫 화면 구성하기

- 앱을 켰을 때 바로 게임이 시작되지 않도록 첫 페이지를 만들어준다.

 

MainActivity

package com.example.step09gameview;

import android.content.Intent;
import android.os.Bundle;
import android.widget.Button;

import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button startBtn=findViewById(R.id.startBtn);
        //게임 시작 버튼을 누르면
        startBtn.setOnClickListener(view -> {
            //Game 액티비티로 이동해서 게임이 시작되도록 한다.
            Intent i=new Intent(MainActivity.this, GameActivity.class);
            startActivity(i);
        });

        //소리를 재생할 준비를 한다.
        SoundManager sm=new SoundManager(this);
        sm.addSound(1, R.raw.laser1);
        sm.addSound(2, R.raw.birddie);
        sm.addSound(3, R.raw.shoot1);

        Button playBtn=findViewById(R.id.playBtn);
        playBtn.setOnClickListener(view ->{
            sm.playSound(3);
        });
    }
}

 

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/startBtn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Game Start"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/playBtn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="17dp"
        android:layout_marginTop="64dp"
        android:text="효과음 재생"
        app:layout_constraintStart_toStartOf="@+id/startBtn"
        app:layout_constraintTop_toBottomOf="@+id/startBtn" />

</androidx.constraintlayout.widget.ConstraintLayout>

 

- 게임을 시작하는 화면 액티비티 추가

new Activity- Empty

 

 GameActivity

package com.example.step09gameview;

import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;

public class GameActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //GameView 객체를 생성해서
        GameView view=new GameView(this);
        //MainActivity 의 화면을 GameView로 모두 채운다.
        setContentView(view);
    }

    //옵션 메뉴를 만드는 메소드
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        //메뉴 전개자 객체를 얻어와서
        MenuInflater inflater=getMenuInflater();
        //res/menu/menu_option.xml 문서를 전개해서 메뉴로 구성한다.
        inflater.inflate(R.menu.menu_option, menu);
        return true;
    }
    //옵션 메뉴를 선택했을 때 호출되는 메소드
    @Override
    public boolean onOptionsItemSelected(@NonNull MenuItem item) {
        switch (item.getItemId()){
            case R.id.off: //off를 눌렀을 때 해야할 동작

                break;
            case R.id.on: //on을 눌렀을 때 해야할 동작

                break;
        }
        return super.onOptionsItemSelected(item);
    }
}

 

- main에 있던 Gameview 객체 생성 내용을 GameActivity로 옮기고,

  Main에 는 게임 시작 버튼을 만들것이다.

 

- setOnClickListener 메소드 하나짜리 인터페이스를 익명클래스로 오버라이드한 것

 

- 이제 버튼이 있는 Main이 첫화면으로 나온다.

- 클릭하면 그때 게임 화면으로 들어가진다!

 

 


 

- 효과음 넣어주기

 

SoundManager 클래스 생성

package com.example.step09gameview;

import android.content.Context;
import android.media.AudioManager;
import android.media.SoundPool;

import java.util.HashMap;
import java.util.Map;

/*
    효과음을 필요한 시점에 재생하기 위한 클래스 설계
 */
public class SoundManager {
    //사운드의 아이디값(정수값) 을 저장하기 위한 Map
    Map<Integer, Integer> map=new HashMap<>();
    //SoundPool
    SoundPool pool;
    //Context
    Context context;
    //볼륨
    int streamVolume;

    //생성자
    public SoundManager(Context context){
        this.context=context;
        pool=new SoundPool(1, AudioManager.STREAM_MUSIC, 0);
        //오디어 서비스 객체를 얻어와서
        AudioManager am=(AudioManager)
                context.getSystemService(Context.AUDIO_SERVICE);
        //설정된 음악 볼륨값을 읽어와서 필드에 저장한다.
        streamVolume=am.getStreamVolume(AudioManager.STREAM_MUSIC);
    }

    //재생할 사운드 등록하는 메소드
    public void addSound(int key, int resId){
        //resId 를 이용해서 사운드를 로딩하고 아이디값을 리턴 받는다.
        int soundId=pool.load(context, resId, 1);
        //리턴 받은 아이디값을 인자로 전달된 키값으로 저장한다
        map.put(key, soundId);
    }
    //사운드를 재생하는 메소드
    public void playSound(int key){
        //인자로 전달받은 키값을 이용해서 Map 에서 재생할 사운드의 아이디를 읽어온다.
        int soundId=map.get(key);
        //재생하기
        pool.play(soundId, streamVolume, streamVolume, 1,0, 1);
    }
    public void stopSound(int key){
        int soundId=map.get(key);
        pool.stop(soundId);
    }
    public void pauseSound(int key){
        int soundId=map.get(key);
        pool.pause(soundId);
    }
    public void resumeSound(int key){
        pool.resume(map.get(key));
    }
    //자원 해제 하는 메소드
    public void release(){
        pool.release();
    }
}

 

- res/raw폴더에 효과음이 들어있다.

- res 폴더 안에 들어있는 모든파일은 영문자 소문자로만 이루어져 있어야 한다!

 

- SoundPool() : 짧은 효과음을 재생하기위한 메소드

 

- SoundManager 클래스에 객체등록

 

- addSound : 메소드에 키, 밸류 값을 등록해서 재생할 준비를 해준다.

 

- 재생해야 할 타이밍에서 playSound 메소드를 호출하면서 저장된 키 값 전달하기!

 

- 아래의 추가 메소드들은 사운드를 정지하거나 멈추고 싶을때, 객체 관리에서 해제할때 사용

 

 

- Activity에 사운드용 버튼 추가하기!

 

MainActivity

- 키 값을 넣어서 빼서 쓰는데 사용한다.

- 정수값과 함께 hashmap으로 관리하고 있다.

 

- 버튼을 누르면 특정 소리가 나도록 설정했다.

- 숫자를 키값으로 저장해두면 기억하기 어려우므로,

 키 값을 적절한 이름으로 static final 상수로 정의하고 사용하면 된다.

 

- 이 컨텍스트에 activity의 참조값을 전달해서 객체 생성을 하고,

 객체를 생성하는 시점에 SoundPool 객체를 생성하는데,

 생성자의 인자로 STREAM_MUSIC과 음질 등등의 정보를 전달한다.

- AudioService 를 사용해서 음악의 볼륨 값을 필드에 저장한다. 그 볼륨 크기로 재생한다.

 


 

- res/values 폴더가 있는데 자원을 여기에 저장해두고 가져와서 사용할 수도 있다.

- 장점: 언어별로 문자열을 따로 저장해놓고 국가의 위치에 따라서 다른 문자열을 제공하게 할 수도 있다.

 

- res에 우클릭 - new - android Resource FIle

 

 

menu_option 생성 (menu 타입)

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:id="@+id/off" android:title="사운드 off"/>
    <item android:id="@+id/on" android:title="사운드 on"/>
</menu>

 

- 새로운 자원이 만들어졌다.

 

- item 요소 생성하고 id, title을 부여해준다.

 

 

GameActivity에서 잓성

- onCreateOptionsMenu 메소드로 가져오기

 

- 이렇게 작성해주면 게임 우상단에 메뉴가 생긴다.

 

- inflater 전개자 메소드로 무엇을 전개할 것인지, 무엇을 합칠것인지 여기에 적어주면 된다.

- xml문서에 옵션을 나열하고, 메뉴 전개자 객체를 사용해서 전개하면 된다.

 

- 사운드를 on,off 하거나 게임을 일시정지하는 메뉴도 만들 수 있다. 이것이 optionMenu 이다!

 

- 어떤 것을 선택했는지 읽어오기. 옵션 선택에 대한 구별(리스너)

- onOntionsItemSelected() 메소드를 override 해서 작성해주면 된다.

 

- switch 를 사용하고, id값을 읽어와서 안에 원하는 동작을 넣어준다.