adsense



[JAVA] 2D 슈팅게임 개발강좌- 위치즈 플라이트 보스전!



무려 8년여만의 위치즈 플라이트 추가 수정입니다..!


유니티니 언리얼이니 고도니 코코스니
강력한 게임엔진을 얼마든지 골라잡을 수 있는 시대에,

순수 자바만 갖고 게임을 만드는 것에 관심을 보여주셔서 감사합니다.



요즘 그 많이들 여는 강좌 같은거 해 볼까 했는데 수요 없을 것 같죠..




우선, 저도 거의 6년만에 이클립스 다시 깔고 테스트 해 보니 기존 샘플게임에서 수정할 곳이 좀 있었네요.

1. 키 입력이 제대로 동작하지 않는 경우가 있었습니다.

TitleScene, GameScene, ResultScene 각각의 생성자에
addKeyListener(this);

를 추가해주면 일단 임시변통은 됩니다. (화면 전환 때마다 창을 마우스로 한 번 클릭해줘야 하는 불편함은 남습니다..)

다른 버전의 코드에선 고쳤던가 저도 기억이 감감..


2. 창 크기가 이상해서 그림이 많이 잘립니다.

GameManager.java의 setBounds 항목 두 군데를 수정해주고,
하는 김에 firstScene 함수도 sceneChange함수를 통하도록 통일했습니다.

대충 수정 전, 수정 후 비교입니다.


이번 포스트에 동봉한 최종 수정본 코드에서는 적용되어 있으니 기존 버전과 비교해 보셔도 되겠네요.

최종 수정본 받기WFlight20211112.zip






그럼 이번 포스트의 본 목적인 보스 추가를 시작하겠습니다.

- 보스가 등장할 조건이 갖추어지면
- 보스가 나와서 싸우고,
- 보스를 파괴하면 클리어? 승리? 화면으로 넘어가서 게임이 끝나게 됩니다.

※게임 기획적으로는 문제가 있습니다. 이 게임은 기본적으로 최대한 멀리 가기 위해 리네를 레벨업 하는 게임이거든요.
보스를 처치하면 게임이 끝난다는 건, 기본 시스템의 취지와 어긋납니다.
게임 성격에 맞추자면, 보스를 처치하면 게임이 속행되고 다음 조건이 갖춰지면 더 강한 보스가 나타나고.. 하는 식으로 되는게 맞겠죠.

하여튼, 위 조건을 갖고 진행해 보겠습니다.





보스도 기본적으로는 일반적인 적 캐릭터와 다를 바 없습니다.

보스가 갖는 특징을 생각해보면

- 대체로 크다.
- 화면에 계속 남아있는다.
- 강력한 공격을 해 온다.
- HP가 많다.

정도가 있을까요.

이 게임에서 적 캐릭터는 기본적으로 공격은 해 오지 않기 때문에 공격 부분은 따로 고민해야겠습니다만,

다른거라면 이미 Enemy 클래스로 충분히 구현이 가능합니다.


우선 어떤 보스로 할까.. 옛날에 구현하다 만게 있습니다.

[영상]


cocos2d-x 엔진 사용해서 본격적인 상용게임으로 하려다가 아무리 2차창작이 회색지대라고 해도 이건 좀 아니다 싶어서 접은건데,

그대로는 아니고, 이 그림만 조금 써서 아주 간단히 구현해 보겠습니다. 영상처럼은 되지 않으니 양해를.

우선 아래 그림 파일을 rsc/game에 저장해 주세요.
(좌우 이동은 생각하지 않고 화면 좌우에 딱 맞는 크기로 처음부터 그렸던 겁니다)

x-3.png

그리고 GameScene의 initImage와 releaseImage에서 각각 로드/해제 코드를 추가합니다.
Image boss;

public void initImage() {

[중략]
boss = manager.makeImage("rsc/game/x-3.png");//보스
}

public void releaseImage() {

[중략]
boss = null;//20211111
}


Enemy 클래스의 생성자를 다시 보면

public Enemy(Image pic, Image pic2, Rectangle rect, int x, int y, int hp, int speed, int kind)

이건 원래 거대 미사일을 출현시키기 위해 추가한 생성자죠.
어쨌거나 이걸 이용할 수 있습니다.

Enemy bosschr = new Enemy(boss, null, new Rectangle(8,60,464,222), 0, -1074, 100, 6, 2);
정도로 생성할 수 있겠네요.

생성자에 넘겨준 인수는 다음과 같은 의미입니다.

boss : 보스의 Image 객체입니다.
null : 현재는 두 번째 그림이 없으므로 null을 넣습니다. (나중에 이쪽은 변경됩니다)
new Rectangle(8,60,464,222) : 보스의 피격 범위입니다. 아래 그림의 녹색 선에 해당합니다. (실제 그림에는 선은 보이지 않습니다)
0, -1074 처음 등장 좌표입니다. y가 왜 -1074이냐면 화면 밖에서 아래로 죽 내려올거라서입니다.
100 : 보스의 HP입니다. 테스트하면서 적당한 값으로 바꿔주면 됩니다.
6 : 움직이는 속도입니다.
2 : 식별값(kind)입니다.


kind가 2인데, 기존의 kind는
0이 일반 네우로이
1이 V2형 네우로이입니다.
그러므로 여기 없는 2를 보스로 하기로 정한겁니다.

----------------------------------------------------------------------

보스가 출현할 조건을 만들어 보겠습니다.

출현 조건은 간단하게, GameScene에서 levelup이 호출되면 보스가 나오도록 합니다.


우선 보스 등장을 처리할 함수 CreateBoss를 다음과 같이 GameScene.java 맨 아래에 만들어 줍니다.
void CreateBoss() {
Enemy bosschr = new Enemy(boss, null, new Rectangle(8,60,464,222), 0, -1074, 100, 6, 2);
enemies.add(bosschr);

void levelup()은 이렇게 수정됩니다.

void levelup() {
if (_level < 5)
_level++;// 레벨은 5까지
if (_speed < 20)
_speed += 2;
CreateBoss();


이제 보스 등장 준비가 끝났습니다.

그리고, 보스가 등장 중이라는 것을 표시할 플래그 변수가 필요합니다.
보스가 중복 등장하는 것을 막거나, 보스가 나와있는 동안에는 일반 적 캐릭터의 생성을 막거나 하는 용도로 쓰기 위한 것입니다.

GameScene의 앞부분에
boolean isBoss;

SceneStart에
isBoss = false;
를 각각 추가하고

CreateBoss를

void CreateBoss() {
if(isBoss)
return;
Enemy bosschr = new Enemy(boss, null, new Rectangle(8,60,464,222), 0, -1074, 100, 1, 2);
enemies.add(boss);
isBoss = true;

이렇게 수정해 줍니다.

그리고, 일반 적 캐릭터를 등장하지 않게 할 것입니다.

processEnemy 함수에서
적 캐릭터를 생성하기 위한 판단을 하는 일련의 코드들을 
if(!isBoss){
[기존 코드]
}
이렇게 감싸줍니다.
정확한 내용은 샘플 코드를 참조해 주세요.


----------------------------------------------------------------------

이제부터는 Enemy 클래스에 추가 작업을 합니다.


-draw에 보스 캐릭터를 그려줄 부분을 추가합니다
case 2:
gContext.drawImage(pic, x, y, _ob);
break;

-process에서
if(kind==0)
y+=speed;
else if(kind==1){
if(cnt>=150)//자체 카운트가 150에 도달하기 전까지는 경고만 나오므로 이동이 없다 
y+=(speed*15/10);//1/.5배 빠르다
else
return NO_PROCESS;//경고만 보여지고 있는 동안에는 충돌 체크가 없다
}
이 부분을 찾아 그 뒤에
else if(kind==2){
if(y<100)
y+=speed;
}
를 추가합니다.

처음 생성된 y위치인 -1074에서 100까지 내려오는 것입니다.
내려오는 속도가 너무 느리면 앞의 CreateBoss에서 스피드를 늘려 봅니다.

이제 kind에 의한 분기가 늘어나니까 슬슬 여기를 switch 문으로 바꾸겠습니다.


그래서 수정하면 process의 내용은 이렇게 됩니다.
switch(kind) {
case 0:
y+=speed;
break;
case 1:
if(cnt>=150)//자체 카운트가 150에 도달하기 전까지는 경고만 나오므로 이동이 없다
y+=(speed*15/10);//1/.5배 빠르다
else
return NO_PROCESS;//경고만 보여지고 있는 동안에는 충돌 체크가 없다
break;
case 2:
if(y<-100) {
y+=speed;
}
break;

여기까지 잘 됐다면 실행해보면 대충 적 편대가 몇 번 지나가고 나서 보스가 나옵니다.

아무것도 안하지만 대충 총알 좀 맞다 보면 뿅 사라집니다.


엄청 허무하죠. 통나무 임금님이에요.



좀 보스답게 꾸며보겠습니다.



- 파괴될때는 나름 화려하게

파괴 판단과 처리는 GameScene에서 하니까 다시 여기를 봅니다.
void processBullet()에 있습니다.

if (_temp.hp <= 0) {
// 적의 HP가 바닥나 파괴 처리
(중략)
}

여기를
if (_temp.hp <= 0) {
// 적의 HP가 바닥나 파괴 처리
if(_temp.kind==2){
}
else{
(중략)
}

이렇게 수정합니다. (※원래 Enemy클래스의 kind는 private 속성이니 이것을 public으로 수정해 줍니다)


보스는 HP가 0이 되면 지속적으로 폭발하면서 화면 아래로 밀려 내려가서 사라지는 것으로 하겠습니다.

일단 Enemy 클래스에서는 파괴 여부를 확인할 수 없고,
GameScene에서는 Enemy의 움직임을 제어하지 않으므로 편법을 쓰겠습니다.

if (_temp.hp <= 0) {
// 적의 HP가 바닥나 파괴 처리
if(_temp.kind==2){
_temp.SetDestroy();//<-여기 이것
}
else{
(중략)
}
}

SetDestroy는 Enemy클래스에 만들어 줍니다. 그 내용은

public void SetDestroy(){
kind = 3;
rect = null;
}

kind==3은 '파괴된 보스 캐릭터'라고 하기로 합니다.
새로 생성하는건 아니고, 현재의 보스 캐릭터의 kind 값을 바꾸게 됩니다.

파괴 상태에서 총알에 맞거나 내려오면서 플레이어와 충돌하면 안되므로 판정사각형을 아예 null로 만듭니다.


그리고 Enemy클래스의 draw와 process에서 kind==3을 처리하기 위해 switch 안에 추가 코드를 넣습니다.

draw
case 3://20211111
gContext.drawImage(pic, x, y, _ob);
break;


process

case 3:
y+=3;
if(y < 600 && cnt%4==0) {//아직 보스가 충분히 위에 있으면 4프레임마다 폭발 이펙트를 호출합니다
//폭발 이펙트 호출
}
if(y > 700) {//보스가 충분히 아래로 내려갔으면 씬을 전환합니다
//클리어 씬 전환
}
return NO_PROCESS;//파괴된 보스는 충돌 체크가 없다

break;가 아니라 return NO_PROCESS;로 끝나는 것에 주의(?)



이펙트는 GameScene에서 관리합니다.

현재는 Enemy클래스에서 GameScene을 바로 억세스할 방법이 없으니
GameScene객체를 다룰 수 있도록 수정합니다.

Enemy의 클래스 변수에
public GameScene gameScene;
을 추가하고

GameScene의 CreateBoss에서
bosschr.gameScene = this;
를 추가합니다.




그리고 다시 GameScene에 함수를 둘 추가합니다

하나는 폭발 이펙트를 그려주기 위한 함수 (이펙트를 모두 GameScene에서 관리하기 때문에 이렇게 하는게 편합니다)
public void DrawEffect(int x, int y) {

x+=manager.RAND(-200,200);
y+=(manager.RAND(-100,100)+180);//폭발이 일어나는 위치를 랜덤하게 해 줍니다
Effect _effect = new Effect( effect, x, y, 6, 4);//이 이펙트 자체는 기존의 적 캐릭터 파괴 시 표현해주던 것과 같은 것입니다
effects.add(_effect);
}

다른 하나는 클리어 화면으로 전환하기 위한 함수입니다. 결과 화면으로 전환하는 것과 같습니다.

public void CallClearScene(){
Destroy();
manager._getGold =_gold;
manager._getRange = _range;
manager._getScore = _score;
//manager.sceneChange((GameCanvas)new ClearScene(manager));
manager.sceneChange((GameCanvas)new ResultScene(manager));//※ 아직 클리어 씬을 만들지 않았으니까 임시로 ResultScene으로 보냅니다.
}

 이상과 같이 수정합니다.


그리고 마지막으로 Enemy클래스의 process에서
case 3:
y+=3;
if(y < 600 && cnt%4==0) {
//폭발 이펙트 호출
gameScene.DrawEffect(x+240, y);//<-여기랑
}
if(y > 700) {
//클리어 씬 전환
gameScene.CallClaerScene();//<-여기가 추가
}
return NO_PROCESS;//파괴된 보스는 충돌 체크가 없다

이렇게 되겠네요.

이제 보스를 격파하면 제법 화려하게 터지면서 추락?하고, 충분히 내려가면 화면이 바뀝니다.

이것으로 최초의 목표-보스를 출현시키고 보스를 처치하면 클리어 화면으로-는 대충 다 달성했습니다.

하지만 여전히 아쉬움이 있습니다. 죽기만 멋지게 죽지 하나도 보스답지 않아요.



보스를 보다 보스답게

-보스의 HP표시
-보스의 공격
-파괴된 보스로부터 대량의 아이템이 드롭

이 세 가지를 추가하겠습니다.

보스의 HP표시
이건 지금까지 듀얼샷이나 마그넷의 남은 시간을 막대그래프로 표시하는 것과 같습니다.

아래 이미지를 rsc/game 폴더에 추가하고, Image객체를 선언하고, GameScene의 initImage와 releaseImage에서 각각 로드/해제 코드를 추가합니다.
(코드는 생략, 첨부를 참고해주세요)


그리고, CreateBoss에서 보스를 생성할 때, 두 번째 이미지 인수로 이걸 넣어줍니다.

Enemy bosschr = new Enemy(boss, bossHpBase, new Rectangle(8,60,464,222), 0, -1074, 100, 6, 2);

bossHpBase가 그것입니다.

그리고, 다시 Enemy 클래스에서,
우선 HP최대치(초기치)를 기록할 필요가 있습니다.

int hpMax;

이 변수를 선언해 주고요,

생성자
public Enemy(Image pic, Image pic2, Rectangle rect, int x, int y, int hp, int speed, int kind){
에서 맨 마지막에
this.hpMax = this.hp;
를 추가해 줍니다.

그리고, draw 할 때 kind==2 인 경우, 이 이미지를 그려줍니다.

case 2:
gContext.drawImage(pic, x, y, _ob);
//HP바 표시
if(kind==2) {
gContext.drawImage(pic2, 25, 97, _ob);//게이지 바탕을 그립니다. pic2가 앞에서 받은 bossHpBase 입니다.
gContext.setColor(new Color(255,0,0));//다음에 그릴 요소의 색을 정합니다. 원하는 색으로 정해보세요.
gContext.fillRect(99, 102, 354 * hp/hpMax, 10);//남은hp/최대hp의 비율로 사각막대를 그립니다. 이게 게이지가 됩니다.
}
break;

kind==3일 때는 그릴 필요가 없으므로 (파괴되어 0이 됐으니) kind==2 일 때만 해 주면 됩니다.



보스의 공격은, 앞의 영상에 나온 것을 간단히 흉내내서 4줄의 빔이 일직선으로 나오도록 할 겁니다.
피할 수 있는 틈이 하나뿐이므로 빔이 나오려는 징조를 잘 보고 빨리 이동해야 하죠.

지금까지 Enemy 클래스의 kind 종류를 보면
0 보통 네우로이
1 V2 네우로이
2 보스
3 파괴된 보스

이번에는 4를 추가하겠습니다.
4는 보스가 쏘는 광선입니다.

'발사 위치에서 잠시 광원이 깜박깜박 한 다음 화면 세로 전체에 빔이 그려진다'
기본적으로는 V2 네우로이와 비슷합니다.
경고 대신 광원이 깜박깜박하고, 경고 타임이 끝나면 이동은 하지 않고 그 자리에 빔이 잠시 존재합니다.
그럼 구현해보겠습니다.

우선, 빔 이미지를 GameScene에 추가해 줍니다.

아래 두 장을 넣어줍니다. 그리고 역시 Image 선언, initImage, releaseImage를 수정합니다. (코드 생략)


보스가 쏘는 것이므로 발생의 판단은 보스가 해야 합니다.
그러므로 Enemy의 process에서 판단합니다.

case 2:
if(y<-100) {
y+=speed;
}
else {
if(cnt%130==129) {
gameScene.BossAttack();
}
}
break;

cnt는 약 1/60초에 1씩 증가하므로 130*(1/60) = 약 2.16초에 한 번 씩 공격을 합니다.
130, 129를 줄여주면 더 자주 공격하고, 크게 해 주면 천천히 공격합니다.

빔 공격은 적 캐릭터의 일종이므로 생성은 GameScene에서 해 줘야 합니다.
그래서 gameScene.BossAttack(); 과 같이 gameScene의 함수를 호출하도록 합니다.
GameScene에 이제 BossAttack을 만들어 줍니다.

public void BossAttack() {

int skipper = manager.RAND(0, 4); //빈 자리를 랜덤으로 지정합니다
for (int i = 0; i < 5; i++) {
if(i==skipper)
continue;//빈 자리에 해당되면 빔을 생성하지 않습니다
Enemy _enemy = new Enemy(bossAttack2, bossAttack,
new Rectangle(23, 0, 35, 700), i * 96, 165, 100, 1, 4);
enemies.add(_enemy);
}
}

Enemy의 생성자 인수는 아래와 같은 의미입니다.

빔은 깜박거리는 광원과 실제 발사되는 광선 두 가지 이미지를 갖고, 적당한 타격판정 사각형을 전달합니다.
hp와 speed는 의미가 없고, kind는 4입니다.



다시 Enemy 클래스에서 kind==4일 때의 draw와 process를 작성하겠습니다.

draw
case 4:
if(cnt<50) {
int sx = 88*(cnt%2);
int sw = sx + 88;
gContext.drawImage(pic2, x, y, x+88, y+88, sx, 0, sw, 88, _ob);//광원이 점멸한다
}
else {
if(cnt%2==0)
gContext.drawImage(pic, x, y, _ob);//빔 본체
}
break;


process
case 4:
if(cnt<50)
return NO_PROCESS;//광원이 보여지고 있는 동안에는 충돌 체크가 없다
if(cnt>100)
return MOVEOUT;//시간이 지나면 사라진 것으로 한다
break;

광선은 처음 발생하면 약 0.8초(50프레임)동안 구체가 깜박깜박하고,
그 이후 길죽한 광선으로 한꺼번에 바뀐 뒤,
다시 약 0.8초(50프레임) 후에 사라지는 처리입니다.

일단 구체 부분은 하나의 이미지에 두 장이 그려져 있지요. 이걸 프레임마다 번갈아서 그려줘 애니메이션 효과를 냅니다.

int sx = 88*(cnt%2);//cnt가 짝수일 때는 0, 홀수일 때는 88이 됩니다.
int sw = sx + 88;//cnt가 짝수일 때는 88, 홀수일 때는 196이 됩니다.

gContext.drawImage(pic2, x, y, x+88, y+88, sx, 0, sw, 88, _ob);//광원이 점멸한다

이건 이미지를 일부만 잘라서 보여주는 코드입니다.
원본 이미지의 sx, 0 부터  sw, 88 사이에 있는 이미지, 그러니까 cnt가 짝수이면 0,0~88,88사이이고 홀수이면 88,0~196,88에 있는 부분을
x, y, x+88, y+88에 그려주라는 뜻입니다.
이렇게 해서 짝수일 때는 큰 광원, 홀수일 때는 작은 광원을 보여주게 됩니다.

다음 

if(cnt%2==0)
gContext.drawImage(pic, x, y, _ob);//빔 본체

짝수일때는 빔을 그리고 홀수일때는 그리지 않습니다.
이렇게 하면 빠르게 반짝거리는 점멸효과를 줄 수 있습니다.

설명이 길어졌지만 내용은 그리 복잡하지 않습니다.


process의 코드는 간단합니다. 아직 광원일 때는 충돌체크를 하지 않고, 약 1.6초가 지났으면 GameScene에 이걸 지워주세요 리턴값을 보내서 자신을 소멸시킵니다.


이만하면 그럴싸한 공격이 만들어졌는데, 조금 이상합니다?

광원이 번쩍일때도 총알이 뭔가 보이지 않는 벽에 맞고 있습니다?

바로 빔에 맞고 있는 겁니다.


총알과 총알이 부딪힐 수도 있지만 이 경우는 어색하죠. 그래서 Bullet 클래스를 수정해서 총알이 빔과 충돌하지 않도록 하겠습니다.

Bullet클래스의 process에서

public int process(Vector enemies){

int ret = NO_PROCESS;
//적 캐릭터와의 명중을 판단한다
Enemy _buff;
for(int i=0;i<enemies.size(); i++){
_buff = (Enemy)enemies.elementAt(i);
//20211111
if(_buff.kind==4)
continue;
//20211111
if(RectCheck.check(x, y, rect, _buff.x, _buff.y, _buff.rect)){
ret = i;
break;
}
}
//이동 처리한다
y-=16;
if(y<=-10)
ret = MOVEOUT;
return ret;
}

붉은 부분을 추가합니다.
자 이제 보스 공격이 완성됐습니다. 어디로 피해야 하는지 빠르게 판단해야 하므로 제법 어렵습니다?



마지막으로 폭발하는 보스로부터 아이템이 무더기로 튀어나오는 처리입니다.

간단합니다. 보스의 폭발 이펙트를 그려줄 때, 동시에 코인/빅코인 아이템도 나오게 해 주면 됩니다.

GameScene의 DrawEffect를 아래와 같이 수정합니다

public void DrawEffect(int x, int y) {

x+=manager.RAND(-200,200);
y+=(manager.RAND(-100,100)+180);
Effect _effect = new Effect( effect, x, y, 6, 4);
effects.add(_effect);

int itemkind = 0;
if(manager.RAND(0, 10)==7)
itemkind = 1;
Item newitem = new Item(manager, itempic[itemkind], x, y, itemkind);
items.add(newitem);
}

폭발 위치에 코인을, 1/10 확률로 빅코인(보석)을 생성합니다.

그럼 여기까지 하고, 최종 수정본을 사용한 영상을 한 번 보시죠.
이 영상에는 추가 ClearScene까지 만들어 놨습니다. ResultScene을 그대로 복사해서 그림과 좌표만 조금 수정했습니다.



와우 진짜 보스같죠..!?



그럼 이것으로 오랜만의 자바 슈팅 게임 개발강좌를 마치겠습니다.




보너스 팁?

CallClaerScene()에서 씬 전환 내용을 싹 빼버리고
isBoss = false;
이거 하나만 수정해 보세요.

일정 간격으로 보스가 등장하고 보스를 처치하면 난이도가 올라가는, 그런 게임이 됩니다.
원래의 게임 의도대로라면 이쪽이 더 어울리지 않을까요.

이렇게 하면
CreateBoss 할 때 _level에 따라 HP를 증가시켜서 난이도를 높일 수 있고,
낮은 레벨에서는 빔을 적게, 천천히 쏘고 높은 레벨에서는 빔을 많이, 빠르게 쏘는 식으로 개조해 봐도 좋겠습니다.


그럼 이것으로 정말 끝!

감사합니다.




덧글

  • 펭귄대왕 2022/08/10 03:58 # 답글

    완성된 프로젝트는 아래에서 받을 수 있습니다
    https://github.com/ChangseOh/WitchesFlightJava
댓글 입력 영역


Books

Geek라이프

기초부터 시작하는 모형 전자공작
박성윤 저

패미컴 퍼펙트 카탈로그
마에다 히로유키 저/조기현 역

MSX&재믹스 퍼펙트 카탈로그
마에다 히로유키 저/조기현 역

핵심강좌! Cocos2d-x
이재환 저
예스24 | 애드온2
일본서적 전문사이트 NEPIC