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 에 일정한 숫자를 넣어서 날개가 움직이게 한다.
- 너무 자주 바뀌지 않도록 적절한 수를 넣어주면 된다.
'국비교육(22-23)' 카테고리의 다른 글
74일차(1)/Android App(25) : Kotlin 연산자 / apply / when (0) | 2023.01.20 |
---|---|
73일차(3)/Android App(24) : View 로 미니 슈팅게임 만들기(3) (0) | 2023.01.19 |
73일차(1)/Android App(22) : Kotlin forEach, filter, map / set, get 함수 (0) | 2023.01.19 |
72일차(5)/Android App(21) : View 로 미니 슈팅게임 만들기(1) (0) | 2023.01.19 |
72일차(4)/Android App(20) : Custom View (0) | 2023.01.19 |