2019년 8월 4일 일요일

GMS2 적 움직임 만들기 state machine이용 ( enemy movement with state machine )


서론

이번에 만드는 최종 내용은 아래 이미지입니다.  플래폼 게임에서 총알을 발사하는 적이 이번에 만들 내용입니다. (with GMS2)


알아야 할것


Solid

플래폼게임에 있어서 바닥이 되는 object를 solid라고 표현합니다. 바닥으로는 점프할때 부딧히기도 하면 이것은 벽이 되기도 합니다.
여기에서는 oParSolid 라는 object가 바닥을 나타냅니다.

State machine(상태 기계)

적의 움직임에서 State는 총쏘기 움직이기 등으로 나뉩니다. 이 움직임을 구현하기 위해서는 매 프래임(Step) 마다 들어올때 적절한 상태(State)를 변화를 시키게 되는데 이것을 상태 기계라고 불립니다.


sprites & resource


sprites에는 플랫폼 블럭이 필요하고 적이미지(idle일때와 걸어 다닐때 이미지) 총알 sprite모두 4가지를 준비하였습니다.

room0

room에는 적과 플랫폼이 되는 블럭 하나만 올려두었습니다.


Objects


oParSolid


물체가 이동하지 못하게 하는 용도로 여기에서는 이미지를 block 형태로 사용하였고 특별한 코드는 없습니다.

oEBullet


적이 쏘는 총알에 대한 object입니다. 여기도 특별한 코드는 없고, 총알의 이미지만 있습니다.

oParBullet


oEBullet의 부모가 됩니다. 여기에는 Outside View 에 대한 이벤트 코드인 화면벗어날때 객체를 소멸하는 코드만 들어있습니다. 이렇게 되면 oEBullet 에 상속이 되어 적용됩니다. 여러 종류의 총알을 만들기 위한 용도이나 여기 예제에서는 하나밖에 없습니다.

oAlienSectoid2


핵심이 되는 외계인 object입니다. 기본적인 코드들은 다른 platformer 소스에서 가져와서 정확한 출처는 알 수가 없고 제가 작업한 부분은 state machine 되는 부분만 작업을 하였습니다. Destroy이벤트는 특별한 내용이 없습니다.

코드 설명

Create

// HP
max_hp = 30
hp = max_hp

// Movement
groundAccel = 0.03;//1.00;
groundFric  = 0.05;//2.00;
airAccel    = 0.5;//0.75;
airFric     = 0.05;//0.01;
vxMax       = 1.0;//5.50;
vyMax       = 8;//10.0;
jumpHeight  = 6.00;
gravNorm    = 0.25;//0.50;
gravSlide   = 0.15;//0.25; 

// Velocity
vx = 0;
vy = 0;

platformTarget = 0

facing = 1

state = SM_WALK
need_to_change_state = true

state_count = 0;
state_count_max = 30;
state_save = -1;
max_hp, hp 값은 체력을 표시하기 위한 용도인데 이 예제에서는 사용하지 않습니다.
// Movement 와 Velocity 부분은 플랫폼 소스에 기본 예제로 있던 부분입니다. 간략하게 설명드리자면  Movement값은 object의 움직임에 대한 parameter로 사용합니다. 점프는 얼마나 할지, 움직임의 최대속도는 얼마가 되는지 가속도는 얼마가 되는지를 설정하게 됩니다.
vx, vy 는 해당 object의 속도를 나타냅니다. Step단에서 속도가 정해지면 EndStep에서 움직임 거리 x,y를 계산해 냅니다.
platformTarget은 이 object가 상대편 과 부딧혔을때의 고유 id 정보가 넘어오게 됩니다.
facing은 이미지를 flip 시키기 위한 용도로 사용합니다. 1,-1 값을 지닙니다.
state : state machine의 가장 중요한 상태값입니다. 넣는값은 scripts macro안에 있습니다.
need_to_change_state 가 true가 되면 다음 step에 state가 변경됩니다.
state_count 기본적으로 state는 일정 시간 후에 자동으로 변경되는데 이 값을 count 하기 위한 값이며, 해당 값이 state_count_max 가 되면  need_to_change_state 값이 true가 되되록 되어있습니다.
state_save 는 임시로 저장된 이전 state 값입니다.

Begin Step

/// @description Insert description here
// You can write your code in this editor

onGround = OnGround();

cLeft  = place_meeting(x - 1, y, oParSolid); 
cRight = place_meeting(x + 1, y, oParSolid);

dRight = !((place_meeting(x+sprite_width+1,y+1,oParSolid)));
dLeft = !((place_meeting(x-sprite_width-1,y+1,oParSolid)));
onGround는 object가 플랫폼 위에 있는지 검사합니다. 만약 해당 값이 false이면 하늘이 떠있는것입니다. 그렇다면 아래로 떨어지는 구현이 필요합니다.
cLeft, cRight는 각각 왼쪽 오른쪽에 객체를 넣을 수 있는지 점검 합니다. 즉 옆에 벽이 있는지 점검합니다. 벽이 있는 경우 못가도록 처리가 필요합니다.
dRight, dLeft는 각각 오른쪽 왼쪽 하단에 현재의 object가 떨어질 수 있는지 확인합니다. 해당 값이 true가 되면 그 쪽으로 가면 onGround가 false가 되면서 떨어지게 될것입니다. 이것을 그림으로 표시해보면 아래와 같습니다.


Step

var prev_state = state;
// 상태가 변경해야하는 시점이 되면 상태를 결정하도록 합니다.
if( need_to_change_state ){
 // change state
 // last state
 // change the state as the last state
        // 여기 코드는 다음 상태를 결정하도록 한다.
 switch(state){
  case SM_WALK:
  case SM_WLKR:
  case SM_WLKL:
   state = SM_FIRE
  break;
  case SM_JUMPDOWN:
  case SM_JUMP:
   state = choose(SM_WALK,SM_FIRE)
  break;
  case SM_STAY:
   state = choose(SM_WALK,SM_FIRE)
  break;
  case SM_FIREWAIT:
  case SM_FIRE:
   state = SM_WALK
  break;
 }
 
 // enter state for init code
        // 상태가 변경되자마자 처리해야할 코드들이다. 보통 초기화 코드가 들어갑니다.
 speed = 0
 direction = 0
 state_count = 0
 switch(state){
  case SM_WALK:
   state_count_max = 90
   if( state_save!=SM_WALK && state_save != -1 ){
    state = state_save;
   }else{
    // random
    if (choose(1,2)==1) { 
     state = SM_WLKR;
    }else{
     state = SM_WLKL;
    }
   }
  break;
  case SM_JUMP:
   state_count_max = 120
  break;
  case SM_STAY:
   state_count_max = 30
  break;
  case SM_FIRE:
   state_save = prev_state;
   state_count_max = 10
  break;
 }
 
 need_to_change_state = false
}





// Apply the correct form of acceleration and friction
if (onGround) {  
    tempAccel = groundAccel;
    tempFric  = groundFric;
} else {
    tempAccel = airAccel;
    tempFric  = airFric;
}



// 상태에 따른 처리입니다.
// action
state_count++;
if(state==SM_WLKL || state==SM_WLKR){
 // Left 
 if (state==SM_WLKL) {
     facing = -1;
     // Apply acceleration left
     if (vx > 0)
         vx = Approach(vx, 0, tempFric);   
     vx = Approach(vx, -vxMax, tempAccel);
  if( cLeft || dLeft) {
   vx = 0;
   state=SM_WLKR;
  }
 // Right
 } else if (state==SM_WLKR) {
     facing = 1;
     // Apply acceleration right
     if (vx < 0)
         vx = Approach(vx, 0, tempFric);   
     vx = Approach(vx, vxMax, tempAccel);
  if( cRight || dRight ) {
   vx = 0;
   state=SM_WLKL;
  }
 }
 
}


// Jump 
if (state==SM_JUMP) { 
    if (onGround) {
     vy = -jumpHeight;
    }else{
  state=SM_JUMPDOWN
 }
}
if (state==SM_JUMPDOWN) {
 if (onGround) {
  need_to_change_state = true;
 }
}

if (state==SM_STAY) {
}

if (state==SM_FIRE) {
 if( state_count > state_count_max ){
  var obj = instance_create_layer(x,y,"Instances",oEBullet);
  var dir = 0;
  if( facing == -1 ) dir = 180;
  else dir = 0; 
  with(obj){
   speed     = 5;
   direction = dir;
  }
  state_count = 0;
  vx = 0;
  state=SM_FIREWAIT;
 }
}

if( state_count > state_count_max ) need_to_change_state = true;
// 중력 처리입니다.
// Handle gravity
if (!onGround) {
    vy = Approach(vy, vyMax, gravNorm);
}


End Step


if (vy < 1 && vy > -1)
    PlatformCheck();
else
    repeat(abs(vy)) {
        if (!PlatformCheck())
            y += sign(vy);
        else
            break;
    }

repeat(abs(vx)) {
    if (place_meeting(x + sign(vx), y, oParSolid) && !place_meeting(x + sign(vx), y - 1, oParSolid))
        y -= 1;
         
    if (place_meeting(x + sign(vx), y + 2, oParSolid) && !place_meeting(x + sign(vx), y + 1, oParSolid))
        y += 1;
      
    if (!place_meeting(x + sign(vx), y, oParSolid))
        x += sign(vx);
    else
        vx = 0;
}


Draw

switch(state){
 case SM_WALK:
 case SM_WLKR:
 case SM_WLKL:
  sprite_index = sAlienSectoidRun;
 break;
 case SM_JUMPDOWN:
 case SM_JUMP:
  sprite_index = sAlienSectoidIdle
 break;
 case SM_STAY:
  sprite_index = sAlienSectoidIdle
 break;
 case SM_FIREWAIT:
 case SM_FIRE:
  sprite_index = sAlienSectoidIdle;
 break;
}

//if( oPlayer.x > x ) facing = -1
//else facing = 1

draw_sprite_ext(sprite_index, image_index, x, y, facing * 1, 1, image_angle, c_white, image_alpha);

if(hp<max_hp){
 var my = (sprite_height/2+(sprite_height/2)*(1/4))
 var mx = (sprite_width/2)-(sprite_height/2)*(1/4)
 draw_healthbar(x-mx,y-my-4,x+mx,y-my,(hp/max_hp)*100,c_black,c_red,c_green,0,true,true)
}

Scripts

macro

#macro SM_WALK 10
#macro SM_WLKL 11
#macro SM_WLKR 12
#macro SM_JUMP 20
#macro SM_JUMPDOWN 21
#macro SM_STAY 30
#macro SM_FIRE 40
#macro SM_FIREWAIT 41

Approach

/// @description  Approach(start, end, shift);
/// @param start
/// @param  end
/// @param  shift

if (argument0 < argument1)
    return min(argument0 + argument2, argument1); 
else
    return max(argument0 - argument2, argument1);

OnGround

/// @description  OnGround();

return place_meeting(x, y + 1, oParSolid);

PlatformCheck

/// @description  PlatformCheck();

var collision = instance_place(x, y + sign(vy), oParSolid);

if (collision) {
    if (vy >= 0) {
        platformTarget = collision;
    } else {
        // Platform above
        vy = 0;
    }
    return true;
}

if (vy < 0) {
    platformTarget = 0;
}

if (instance_exists(platformTarget)) {
    if (platformTarget) {
        if (place_meeting(x, y + 1, platformTarget) && !place_meeting(x, y, platformTarget)) {
            //Platform below
            vy = 0;
            return true;
        } else
            platformTarget = 0;
    }
} else
    platformTarget = 0;
    
platformTarget = 0;
return false;


댓글 없음:

댓글 쓰기