[게임 프로그래밍] Steering Behaviors

cocosparticle systemSteering Behaviors 에 대해 공부해볼 것이다.


💡 Cocos Particle System

Base Class
class ParticleSystem(fallback=None, texture=None)

파생 Class

  • class Fireworks(fallback=None, texture=None)
  • class Spiral(fallback=None, texture=None)
  • class Meteor(fallback=None, texture=None)
  • class Sun(fallback=None, texture=None)
  • class Galaxy(fallback=None, texture=None)
  • class Flower(fallback=None, texture=None)
  • class Explosion(fallback=None, texture=None)
  • class Smoke(fallback=None, texture=None)

Cocos Particle class 예제

import cocos
import cocos.particle_systems as ps
class MainLayer(cocos.layer.Layer):
    def __init__(self):
        super(MainLayer, self).__init__()
        particles = ps.Fireworks(fallback = False)
        '''
        fallback: Defaults to None.
        - False: use point sprites, faster, not always available
        - True: use quads, slower but always available
        - None: auto-detect, use the fastest available
        '''
        #particles.angle = 180
        #particles.size = 3
        particles.position = (320, 240) 
        self.add(particles) #레이어에 추가

if __name__ == '__main__':
    cocos.director.director.init(caption='Particle example') 
    scene = cocos.scene.Scene(MainLayer()) #scene에 Layer추가
    cocos.director.director.run(scene) # scene run

Cocos Particle class 메소드

  • active, duration, gravity, angle 등..
  • 가속도의 tangential한지[접선], Radial한지

Particle system 도식화

기존의 CocosNode가 가지는 Sprite, Layer처럼 ParticleSystem노드를 가지고 CocosNode의 메소드를 사용할 수 있다. (do(), pause(), stop(), Kill(), schedule())

💡 Steering Behaviors

총 5단계로 분류할 수 있다.
📌 Seek and Flee
STATIC한 타겟에 대해 쫓아가고 도망가는 것
📌 Arrival
어느 위치에 찾아가는 것, 속도 등에 대한 동작 에 변화를 줄 수 있음.
📌 Pursuit and evade
MOVING하는 타겟에 대해 추격하는 것
📌 Wander
배회 서성거림
📌 Obstacle avoidance
장애물을 회피하는 것

Seek and Flee

상대를 추격하고 쫓아가는 과정에서 두 가지 방법이 있다.

  • 바로 방향 바꾸는 경우 :진행방향 (벡터)만 바꾸어주면 됌
  • 서서히 방향을 바꾸는 경우(기존의 속도를 가지는 경우): 벡터에 대해 조금씩 수정이 필요함

NPC의 멤버

  • Velocity
  • Position
    벡터의 합으로 이동 방향을 점진적으로 바꿔주는 것

Seek and Flee: 추격&도망

Limit Vector

움직일 벡터를 계산하는데 벡터의 리미트값을 정해주어서 최대갈 수 있는 벡터값을 설정한다.
magnitude는 벡터의 크기인데, 파라미터 m보다 크면 벡터를 벡터의 크기로 나누고 m만큼 곱해주어 m크기의 벡터로 만들어준다.

def truncate(vector, m): # limites vector
    magnitude = abs(vector) #크기 -> 루트 x^2+y^2
    if magnitude > m: #크기가 m보다 크면 limit를 m으로 바꾸는 
        vector *= m / magnitude
    return vector

Update : Seek, Flee

Seek의 과정은 Update함수에서 Target 벡터값을 계속 받아와서 구현하는데 내용은 아래와 같다.
1. target벡터에서 self Actor벡터를 빼주어 distance 벡터를 구한다.
2. distance 벡터에서 self가 가지는 기존의 velocity 벡터를 빼주어 우리가 원하는 방향의 벡터인 Steering벡터를 구해준다.
3. steering벡터를 앞에서 제한값을 두는 truncate 함수로 제한 값을 두고 새로운 velocity로 설정한다.
4. position값을 누적해서 더하여 이동한다.

그림을 실행해보면 아래와 같다. 처음 velocity가 위 방향이었다가 target이 오른쪽 아래에 있으니 steering으로 새 velocity를 얻어 이동하고 또 새 target이 생기니 새로운 velocity를 얻어 진행방향을 바꾸어 쫓아온다.

flee의 경우에는 이동 방향을 반대로 하기 위해서 self.seek bool 타입을 두어 false인 경우 -1을 곱해주어 반대 방향으로 이동하도록 한다.

flee의 여러 가지 구현 방법
처음 distance를 구할 때 flee 인 경우 반대 방향으로 distance 벡터로 설정하거나 또는 , steering 벡터를 반대로 설정할 수도 있다.

    def update(self, dt): #update에서 target의 벡터값 계속 업데이트받음.
        if self.target is None:
            return
        distance = self.target - eu.Vector2(self.x, self.y)
        direction = 1 if self.seek else -1 #for flee
        distance *= direction
        steering = distance * self.speed - self.velocity
        steering = truncate(steering, self.max_force)
        self.velocity = truncate(self.velocity + steering, 
                                 self.max_velocity)
        self.position += self.velocity * dt #actor 이동

전체 코드는 아래와 같다.

import cocos
import cocos.euclid as eu
import cocos.particle_systems as ps 


def truncate(vector, m): # limites vector
    magnitude = abs(vector) #크기 -> 루트 x^2+y^2
    if magnitude > m: #크기가 m보다 크면 limit를 m으로 바꾸는 
        vector *= m / magnitude
    return vector


class Actor(cocos.cocosnode.CocosNode):
    def __init__(self, x, y):
        super(Actor, self).__init__()
        self.position = (x, y)
        self.velocity = eu.Vector2(0, 0) #속도
        self.speed = 1 #particle speed
        self.max_force = 5 
        self.max_velocity = 200
        self.target = None
        self.seek = True
        self.add(ps.Sun()) #particle system -> sun particle
        self.schedule(self.update)

     
    def update(self, dt): #update에서 target의 벡터값 계속 업데이트받음.
        if self.target is None:
            return
        distance = self.target - eu.Vector2(self.x, self.y)
        steering = distance * self.speed - self.velocity
        steering = truncate(steering, self.max_force)
        self.velocity = truncate(self.velocity + steering, 
                                 self.max_velocity)
        direction = 1 if self.seek else -1 #for flee
        self.position += self.velocity * dt * direction #actor 이동
        #self.seek인 경우 1 방향, self.seek아닌 경우 -1 방향으로 이동


class MainLayer(cocos.layer.Layer):
    is_event_handler = True

    def __init__(self):
        super(MainLayer, self).__init__()
        self.actor = Actor(320, 240)
        self.add(self.actor)

    def on_mouse_motion(self , x, y, dx, dy):
        self.actor.target = eu.Vector2(x, y) #actor의 target 벡터값을을 마우스 위치로

    def on_mouse_press(self, x, y, buttons, mod):
        self.actor.seek = not self.actor.seek #seek값 반대


if __name__ == '__main__':
    cocos.director.director.init(caption='Steering Behaviors')
    scene = cocos.scene.Scene(MainLayer())
    cocos.director.director.run(scene)

Arrival

Arrival은 Seek와 같은 맥락인데 target과의 거리에 따라 해당 self.actorself.velocity값을 조절해주는 것이다.
self.slow_radius
ramp = min(abs(distance) / self.slow_radius, 1.0) 의 코드로 속도 벡터를 조절해줄 수 있다.

class Actor(cocos.cocosnode.CocosNode):
    def __init__(self, x, y):
    	self.slow_radius = 200

    def update(self, dt):
        if self.target is None:
            return
        distance = self.target - eu.Vector2(self.x, self.y)
        ramp = min(abs(distance) / self.slow_radius, 1.0)
        # 전자가 1.0보다 크면, 1.0으로 제한함.
        steering = distance * self.speed * ramp - self.velocity
        steering = truncate(steering, self.max_force)
        self.velocity = truncate(self.velocity + steering, 
                                 self.max_velocity)
        self.position += self.velocity * dt

실행결과와 같이 타겟과의 거리가 가까워질수록 self의 속도 벡터가 작아지는 값을 얻는다.

Pursuit and evade

이는 움직이는 타겟에 대한 추격과 도망이다. 현재 타겟의 위치를 받아서 그때마다 따라가는 것이 아니라, 일정 시간이 흐른 뒤 타겟이 움직임에 따라 미래에 있을 위치를 연산해서 그 방향으로 Seek하는 방식이다.

    def update(self, dt):
        if self.target is None:
            return
        pos = self.target.position
        future_pos = pos + self.target.velocity * dt
        #기존 위치에 타겟이 향하는 속도 벡터 * 가변시간으로 미래 위치 얻어냄.
        distance = future_pos - eu.Vector2(self.x, self.y)
        steering = distance * self.speed - self.velocity
        steering = truncate(steering, self.max_force)
        self.velocity = truncate(self.velocity + steering, 
                                 self.max_velocity)
        self.position += self.velocity * dt

움직이는 파란색 타겟에 대해 액터가 진행 방향으로 추격하는 모습이다.

Wander

임의로 무작위 랜덤 값을 받아서 움직인다. 이때 그냥 랜덤값을 받으면 부자연스러우므로 움직일 수 있는 거리에 원을 두고 원의 각도에 범위에 제한을 두도록 한다.

class Actor(cocos.cocosnode.CocosNode):
    def __init__(self, x, y):
        super(Actor, self).__init__()
        self.position = (x, y)
        self.velocity = eu.Vector2(0, 0)
        self.wander_angle = 0 #초기 값
        self.circle_distance = 50 #배회 거리에 해당하는 원
        self.circle_radius = 10 
        self.angle_change = math.pi / 4 # 45 degree
        self.max_velocity = 50
        self.add(ps.Sun())
        self.schedule(self.update)

이는 업데이트함수에서 구현을 하는데, 아래와 같다.

    def update(self, dt):
        circle_center = self.velocity.normalized() * \
                        self.circle_distance
        # A, placin circle : self의 속도단위벡터 * 원과의 길이 배수
        dx = math.cos(self.wander_angle)
        dy = math.sin(self.wander_angle)
        displacement = eu.Vector2(dx, dy) * self.circle_radius
        # B, 랜덤으로 얻는 벡터 * 반지름만큼 배수
        self.wander_angle += (random.random() - 0.5) * \
                             self.angle_change
        #배회 각도 제한 : (랜덤 -0.5 ~ 0-.5 )* 45 => -22.5 ~ 22.5
        self.velocity += circle_center + displacement
        # A벡터 + B벡터의 합 -> Steering 속도 벡터
        self.velocity = truncate(self.velocity, 
                                 self.max_velocity)
        self.position += self.velocity * dt 
        self.position = (self.x % 640, self.y % 480)

방향이 무작위로 바뀌는 것이 아닌 일정 방향으로 자연스럽게 배회하는 것을 확인할 수 있다.

Obstacle avoidance

steering을 하다가 장애물이 있는 경우 장애물의 위치에서 방향을 피해서(seek의 방향 반대로) 다시 steering을 하도록 하는데, 이때 장애물이 여러 개 있는 경우에는 self의 가장 가까운 범위 내에 있는 장애물에 대해 피하도록 루틴을 짜도록 한다.

import cocos
import cocos.euclid as eu
import cocos.particle_systems as ps


def truncate(vector, m):
    magnitude = abs(vector)
    if magnitude > m:
        vector *= m / magnitude
    return vector


class Obstacle(cocos.cocosnode.CocosNode):
    instances = [] #static member
    
    def __init__(self, x, y, r):
        super(Obstacle, self).__init__()
        self.position = (x, y)
        self.radius = r
        particles = ps.Sun()
        particles.size = r * 2
        particles.start_color = ps.Color(0.0, 0.7, 0.0, 1.0)
        self.add(particles)
        self.instances.append(self)


class Actor(cocos.cocosnode.CocosNode):
    def __init__(self, x, y):
        super(Actor, self).__init__()
        self.position = (x, y)
        self.velocity = eu.Vector2(0, 0)
        self.speed = 2
        self.max_velocity = 300
        self.max_force = 10
        self.target = None
        self.max_ahead = 200
        self.max_avoid_force = 300
        self.add(ps.Sun())
        self.schedule(self.update)

    def update(self, dt):
        if self.target is None:
            return
        distance = self.target - eu.Vector2(self.x, self.y)
        steering = distance * self.speed - self.velocity
        steering += self.avoid_force()
        # 기존의 steering에 피하는 동작 추가
        steering = truncate(steering, self.max_force)
        self.velocity = truncate(self.velocity + steering, 
                                 self.max_velocity)
        self.position += self.velocity * dt

    def avoid_force(self):
        avoid = eu.Vector2(0, 0)
        ahead = self.velocity * self.max_ahead / self.max_velocity
        # 진행하는 방향벡터 : 속도벡터 * 최대값 / 최대 속도
        벡터
        l = ahead.dot(ahead) #제곱
        if l == 0:
            return avoid #움직이는 경우에만 회피처리
        closest, closest_dist = None, None
        '''
        #장애물 객체 for문처리 : 가장 가까운 장애물 찾음.
        이후 기존의 진행 벡터에서 closet의 벡터를 빼서 avoid벡터를 만들고,
        avoid벡터에 힘 크기만큼 곱해주어 avoid벡터를 수정해서 반환함.
        '''
        for obj in Obstacle.instances: #가장 가까운 벡터 찾음.
            w = eu.Vector2(obj.x - self.x, obj.y - self.y)
            t = ahead.dot(w)
            if 0 < t < l: # l = a*a 진행방향에 어느정도 가까운 범위내에서만 고려
                proj = self.position + ahead * t / l 
                dist = abs(obj.position - proj)
                if dist < obj.radius and \
                   (closest is None or dist < closest_dist):
                    closest, closest_dist = obj.position, dist
        if closest is not None:
            avoid = self.position + ahead - closest
            # 진행하려는 방향의 벡터 - closet 벡터
            avoid = avoid.normalized() * self.max_avoid_force
            # avoid 벡터 * 힘크기를 주어 벡터를 만듦
        return avoid


class MainLayer(cocos.layer.Layer):
    is_event_handler = True

    def __init__(self):
        super(MainLayer, self).__init__()
        self.add(Obstacle(200, 200, 40))
        self.add(Obstacle(240, 350, 50))
        self.add(Obstacle(500, 300, 50))
        self.actor = Actor(320, 240)
        self.add(self.actor)

    def on_mouse_motion(self, x, y, dx, dy):
        self.actor.target = eu.Vector2(x, y)


if __name__ == '__main__':
    cocos.director.director.init(caption='Steering Behaviors')
    scene = cocos.scene.Scene(MainLayer())
    cocos.director.director.run(scene)

좋은 웹페이지 즐겨찾기