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로 화면을 다 채워버리고, 뷰 안에서 자유롭게 코딩해서 게임을 만들 수도 있다.
'국비교육(22-23)' 카테고리의 다른 글
73일차(2)/Android App(23) : View 로 미니 슈팅게임 만들기(2) (0) | 2023.01.19 |
---|---|
73일차(1)/Android App(22) : Kotlin forEach, filter, map / set, get 함수 (0) | 2023.01.19 |
72일차(4)/Android App(20) : Custom View (0) | 2023.01.19 |
72일차(3)/Android App(19) : View Binding (0) | 2023.01.19 |
72일차(2)/Android App(18) : Fragment(2), Fragment Lifecycle (0) | 2023.01.18 |