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값을 읽어와서 안에 원하는 동작을 넣어준다.
'국비교육(22-23)' 카테고리의 다른 글
74일차(2)/Android App(26) : View 로 미니 슈팅게임 만들기(4) (0) | 2023.01.20 |
---|---|
74일차(1)/Android App(25) : Kotlin 연산자 / apply / when (0) | 2023.01.20 |
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일차(5)/Android App(21) : View 로 미니 슈팅게임 만들기(1) (0) | 2023.01.19 |