국비교육(22-23)

72일차(5)/Android App(21) : View 로 미니 슈팅게임 만들기(1)

서리/Seori 2023. 1. 19. 01:07

72일차(5)/Android App(21) : View 로 미니 슈팅게임 만들기(1)

 

- 새 모듈 생성

step09gameview

 

 

- 이런 View로 만드는 게임은 거의 배열 / 반복문 (for문) / if문 이다.

- 슈팅게임의 로직은 대부분 비슷하므로 이미지만 바꿔서 다른게임을 만들 수도 있다!

- 이런 간단한 게임들은 게임엔진없이 View로 가볍게 만들수있다.

 

- 효과음은 compile되지 않고 그대로 있어야해서, res에 raw라는 폴더를 만들어서 넣어주기.

 

 

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;

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;

    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;
    }

    //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;

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

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

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

        //드래곤 그리기
        canvas.drawBitmap(dragonImgs[0], 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;
        }
    }

    //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, 10);
        }
    };
}

 

- View 상속하면 생성자 만드는것이 강제되는데, 2개의 생성자를 만들어준다.

 

 

- 먼저 초기화 메소드에서 배경이미지를 로딩해주기

 

 

- 그런데 문제가 있다. 이 이미지를 읽어온다고 하면 기기별로 해상도, 화면이 다양한데 이미지가 잘릴수가 있다.

→ 배경 이미지의 크기를 화면의 크기게 맞게 조정해주어야 한다.

 

- onSizeChanged() 메소드 오버라이드!

- onSizeChanged() 를 호출한 다음에 초기화 메소드를 호출해야 한다.

 

- 이 원본 이미지를 이런 크기로 만들어서 필드에 저장하겠다는 뜻이다!

 

- 이미지를 0,0 좌표에 채우기!

 

 

MainActivity 에서 화면에 GameView 채우기

package com.example.step09gameview;

import android.os.Bundle;

import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        //GameView 객체를 생성해서
        GameView view=new GameView(this);
        //MainActivity 의 화면을 GameView로 모두 채운다.
        setContentView(view);
    }
}

 

 

- 화면이 이렇게 나타난다. 이 이미지가 화면 전체를 채워야한다!!

- 이제 이미지가 쭉 아래로 내려가는 모양이 되면 내가 이동하는 것처럼 보이게 된다.

 

 

- 내 캐릭터의 위치를 지정하기 위해.. 필드를 새로 만들어준다.

- 유닛의 크기는 화면의 영향을 받는다.

- 화면을 5등분해서 화면의 1/5 크기로 나눠가질 예정! 가운데 위치시킬 것이다.

 

- 드래곤의 초기사이즈는 화면하단에 이만한 크기로 나타나게 할 것이다.

 

- 그런데 위치를 x, y좌표로 지정하면 이 위치에 들어가지지 않고,

 

- x,y좌표 이렇게하면 이만큼 아래에 위치하게된다. 그 지점부터 이미지가 시작되기 때문에!

- 그래서 x,y 좌표에서 유닛 사이즈의 반을 빼주어야 한다.

- 유닛의 폭의 반, 높이의 반만큼 빼주어야 딱 가운데에 들어온다.

 

 

- BitmapFactory로 이미지를 가져오고, 

 이미지 3개를 반복해서 보이게하기(드래곤의 날개가 펄럭거리는 효과)

 

- 그냥 X,Y를 넣어주면 이렇게 그려진다. 왼쪽,위쪽으로 폭의 반씩 이동시켜 주어야 한다!!

 

- 정확히 가운데 위치시키려면 이렇게 폭의 반, 높이의 반으로 값을 넣어주기!

 

- 화면터치에 반응해서 좌우로만 움직이게 하려고 한다.

- 사용자의 터치가 일어난 x좌표를 읽어와서 드래곤의 x좌표에 반영하면 된다.

- 이것이 실시간으로 움직이려면 화면이 업데이트되어야 한다.(다시 그려져야 한다)

 

- 만화,영화,tv등은 초당 최소한 60프레임이상 refresh 되어야 한다.

- 애니메이션효과를 보이려면 onDraw 메소드가 초당 60번이상 실행되어야 한다.

- 화면이 주기적으로 업데이트되는 구조를 만들어야 한다.(invalidate 초기화하기 사용!)

 

- 안드로이드에서만 쓸 수 있는 객체이다. Handler

- 익명클래스로 override 한다.

 

sendEmptyMessageDelayed(0, 10)

- 빈 메세지를 10/1000초 이후에 호출할 것이다. 라는 뜻!

 

- 10/1000초마다 invalidate가 호출된다.

 

- 초기화가 끝난 다음 이 handler를 호출하게 한다!

 

- run해보면 이미지의 좌표 변화가 없기 때문에 움직이는 것이 보이지는 않지만

 1초에 50번씩 갱신되고 있다...

 

- 사용자가 터치하는 정보가 MotionEvent 객체에 들어있다.

 

- float를 리턴하는데 굳이 소수점까진 필요없으므로 int로 받기

 

- 이제 터치하는 x좌표를 이용해서 드래곤 이미지를 움직이게 할 수 있다.

 


 

- 배경 이미지 스크롤효과 넣어주기

- 현재 이 배경이미지의 y좌표가 변하지 않는 상태이다. y좌표를 변화시킬것!

- 이것도 필드로 선언해준다. 배경 이미지 두개가 한번에 변화하게 할 것이므로 필드도 두개 선언하기

 

- 이렇게 배경이미지가 아래로 내려온다.

 

- 무한 스크롤의 원리는 이렇다. 

- 폰의 높이가 height라면 현재 2번의 이미지가 있는 높이는 -height이다.

 

- 1이 언젠가 아래에 도달하고 내려온 2가 1의 위치를 대체하게 된다면

 위에서 1을 다시 출력하면서 -height에서부터 시작하면 된다.

- 이렇게 2개의 이미지를 무한반복한다고 생각하면 된다!

 

- 화면갱신을 좀 특이하게 하고 있다.

- 원래 메인쓰레드에서만 할 수 있는데 메인에서 invalidate하면 앱이 죽어버려서... ??

 

- 메인쓰레드에서 주기적으로 화면을 업데이트할 수 있도록 도와주는 것이 쓰레드이다.

- handler에서 일정 시간 지연된 후에 메시지를 보내게 함으로써 자주 호출된다.(초당 100번)

- 호출되면서 화면을 다시 그림으로서 화면이 바뀐다.

 

- 화면에서 이 dragonImgs 배열을 계속 반복하게 하면

  날개가 펄럭거리는 느낌을 줄 수 있다.(내일 할 예정)

 

 

- 이후에 할 것들:

날개 펄럭이는 표과, 미사일 발사되는 효과, 적기 등장, 적기 미사일 떨어지는 효과,

내 미사일과 적기가 만나면 적기를 화면에서 제거하면서 점수를 올리는 기능, 적기와 부딪쳤을때의 효과 등!

 

- 이처럼 View로 화면을 다 채워버리고, 뷰 안에서 자유롭게 코딩해서 게임을 만들 수도 있다.