Game AI

Game AI - Steering Behaviors (1)

tsyang 2021. 6. 19. 03:12

이전글 : 2021.06.13 - [Game AI] - Game AI - 기본적인 이동

 

Game AI - 기본적인 이동

KINEMATICS 어떤 물체의 운동 상태는 다음과 같이 표현 할 수 있다. public struct Kinematic { public Vector3 position; public float orientation; // x-z 평면에서 +z축으로부터 시계방향 각도. public Vector..

tsyang.tistory.com

가속도 있는 이동 

 


 

이전글의 마지막에서 가속도가 없는 운동을 다뤘다. 가속도 없는 운동의 알고리즘들이 속도를 반환했다면 이번에는 각 가속도 운동 알고리즘이 다음과 같은 가속도를 반환한다.

 

public struct SteeringOutput
{
    public Vector3 linear; // 가속도
    public float angular; // 가속도(각)
}

 

가속도를 반환하다고 해서 모든게 되는건 아니다. 다른 캐릭터를 쫓으면서 장애물을 피하고 근처의 체크포인트까지 들러야 하는 알고리즘은 가속도 운동만으로는 부족하다. 이러한 복잡한 행동을 위해서는 여러 알고리즘을 조합해야 한다.

 


변수 매칭 (Variable Matching)

변수 매칭은 가장 단순한 이동방법 중 하나이다. 변수 매칭은 캐릭터의 동적 상태중 한 요소를 다른 캐릭터의 대응되는 요소와 매칭시키는 것이다.

 

예를 들어, 위치를 매칭시킨다면 대상에게로 가속한 뒤에 대상의 위치에 다다르면 정지하는 것이다. 

 

그러나 만약 속도와 위치를 동시에 매칭시킨다면 어떨까?

 

만약 속도를 매칭시킨다면 서로의 상대적 위치가 변하지 않을태니 위치를 매칭시킬 수 없다. 

 

이런 경우에는 각 요소에 대해 별개의 매칭 알고리즘을 수행하고 나중에 각 요소를 조합하는 방법을 사용할 수 있다.

 


이동 알고리즘

 

private void UpdateKinematic(SteeringOutput steering, float deltaTime)
{
    var oldVelocity = _kinematic.velocity;
    _kinematic.velocity += steering.linear * deltaTime;

    if (_kinematic.velocity.sqrMagnitude > _movementFunc.MaxSpeed * _movementFunc.MaxSpeed)
        _kinematic.velocity = _kinematic.velocity.normalized * _movementFunc.MaxSpeed;

    _kinematic.position += (0.5f * deltaTime) * (oldVelocity + _kinematic.velocity);

    if (_movementFunc.IsFacingMoveDir)
    {
        _kinematic.orientation = ASFunc.OrientationFromVelocity(_kinematic.velocity);
    }
    else
    {
        var oldRotation = _kinematic.rotation;
        
        _kinematic.rotation += steering.angular * deltaTime;
        
        _kinematic.orientation += (0.5f * deltaTime) * (oldRotation + _kinematic.rotation);
        _kinematic.orientation %= 360f;
    }
}

이동 알고리즘은 일단 위와같이 구현했다. 책의 내용에서 방향에 대한 처리만 구분해줬다.

 

매 Update마다 각각의 객체는 이동 방법으로부터 가속도를 얻어온다. 

public void OnUpdate(float deltaTime)
{
    UpdateKinematic(_movementFunc.GetSteeringOutput(), deltaTime);

    UpdateViewTransform();
}

 

 

 


 

항력(Drag)

*참고로 책에서는 항력에 대해 간단히 언급만 한 정도여서 내가 임의로 구현한거라 신뢰성 낮음.

 

항력은 공기저항력이나 유체와의 마찰력같은게 있음. (나는 넓은 의미에서 브레이크도 포함시켰음)

 

그런데 여기서 추후에 공기저항같은 항력(Drag)을 추가한다면 어디에다가 로직을 추가해줘야 할까?

 

처음에는 이동 방법에 추가해줄까 했지만 이동 방법은 이런걸 모르는게 나을 것 같아 일단 그냥 ASObject에 추가해주기로 했다. (ASObject는 이동하는 객체)

 

Drag를 추가할 때 정지하다가 방향이 바뀌지 않도록 주의해야 한다. (브레이크를 밟았는데 차가 정지하다가 뒤로가는 꼴)

 

또한 항력은 속도에 비례해서 커지는데 이 때, 업데이트 되기 전의 속도를 기준으로 해야할까 업데이트 되고난 후의 속도를 기준으로 해야할까?

 

극단적인 상황을 예로 항력이 무한대인 상황을 가정해보자. (콘트리트로 둘러쌓인 것과 같은...) 만약 업데이트 되기 전의 속도를 기준으로 한다면 물체가 정지해 있을 경우 항력이 0이 되므로.. 속도가 상승하고 이동이 발생한다. 즉, 말이 안된다. 따라서 업데이트 될 속도를 기준으로 항력을 적용해야 한다.

 

private void ApplyLinearDrags(float deltaTime)
{
    float speed = _kinematic.velocity.magnitude;
    
    if (speed.FloatEqual(0.0f))
        return;
    
    speed -= _dragCoef * speed * deltaTime;

    if (_isBraking)
        speed -= _brakeDragAccel * deltaTime;

    //항력때문에 방향이 반대로 바뀌면 말이 안되니까
    if (speed <= 0f)
        speed = 0f;

    _kinematic.velocity = _kinematic.velocity.normalized * speed;
}

 

위처럼 항력을 적용하는 코드를 작성했다. 추가적으로 책에서는 SteeringOutput을 null로 반환해서 정지하는 로직을 짠 것 같은데.. 나는 브레이크로 하고싶어서 브레이크도 추가했다. 위 로직을 속도 업데이트와 위치 업데이트 사이에 끼워넣었다.

 

또한, 항력이 있으면 최대 속도를 지정해주지 않아도 된다. (안전빵으로 사용하긴 하지만) 왜냐면 종단속도가 있기 때문이다. 

 

종단속도 예측하기

항력은 단순하게 다음과 같은 식을 썻다. (k는 항력계수(_dragCoef), V는 속도, a는 가속도)

보아하니 속도가 높을때는 V의 제곱에 비례하는 식을 쓰는듯

k는 항력계수, V는 속도, a는 가속도

그럼 종단속도는 다음과 같다.

 

V는 종단속도

즉 최대 가속도가 250이고 항력계수가 0.25면 종단속도는 1000이다.

 

이를 이용해서 최대속도를 어느정도 제어할 수 있다.

 


찾기

 

찾기는 타겟을 향해 전속력으로 움직이는 이동 방식이다. 가속도 없는 운동에서 봤듯이 정지에 어려움을 겪는 알고리즘인데 가속도가 있는 버전에서는 더 쓰기 어렵다. 항력을 사용하면 조금더 안정적인 움직임을 볼 수 있다고 한다.

public class ASObjectMovementFunc_Seek : ASObjectMovementFunc
{
    private readonly ASObject _target;

    protected override SteeringOutput GetSteer()
    {
        var ret = new SteeringOutput();

        var direction = (_target.Position - _character.Position).normalized;
        ret.linear = direction * MaxAccel;
        return ret;
    }
}

 

 

항력 없는 버전

 

 

타겟이 움직이는 경우

 

항력이 없는 버전에서는 아예 멈추지를 못한다. 특히 타겟이 움직인 경우에 일정한 궤도를 도는것을 볼 수 있다.

 

 

항력 추가한 버전 

k=0.25
k=1

 

확실히 항력을 추가한 것이 좀 더 그럴듯한 움직임을 보여준다. k=1 일때가 속도도 그렇고 적당해보여서 앞으로 1을 쓰기로 할 것 같다.

 

 

 


도착

 

가속도 있는 버전에서의 도착은 두가지 거리가 존재한다. 하나는 속도를 줄이기 시작할 거리(SlowRadius) , 하나는 정지할 거리(StopRadius).

 

구현은 아래와 같다.

public class ASObjectMovementFunc_Arrive : ASObjectMovementFunc
{
    public static readonly float TimeToTarget = 0.1f;
    public static readonly float StopRadius = 10f;
    public static readonly float SlowRadius = 100f;
    
    private readonly ASObject _target;
    
    protected override SteeringOutput GetSteer()
    {
        var ret = new SteeringOutput();

        var distVector = _target.Position - _character.Position;
        float dist = distVector.magnitude;

        if (dist < StopRadius)
        {
            ret.isBraking = true;
            return ret;
        }

        float targetSpeed = MaxSpeed;
        
        if (dist < SlowRadius)
            targetSpeed *= dist / SlowRadius;

        var targetVelocity = distVector.normalized * targetSpeed;

        ret.linear = (targetVelocity - _character.Velocity) / TimeToTarget;

        if (ret.linear.magnitude >= MaxAccel)
            ret.linear = ret.linear.normalized * MaxAccel;

        return ret;
    }
}

 

결과

k=1.0
k=1.0

확실히 더 Seek보다 더 그럴듯 하다. 그러나, 책에서는 정지 반경 안에 들어오면 null을 반환해 즉시 멈추게 하는걸 의도한 듯 한데, 나는 즉시 멈추는게 아닌 브레이크 방식을 사용했기 때문에 뭔가 어설프다. 특히 첫 번째 시뮬레이션에서 타겟을 지나치는 문제가 발생한다. 물론 항력과 감속 반경을 적절히 조절해주면 되지만 속도가 더 높아지면 그것도 보장할 수 없다.

 

그래서 정지 반경과, 감속 반경을 동적으로 정해주기로 했다. 

 

정지 반경은 어떻게 구해야 할까? 즉, 어떤 거리 r부터 브레이크를 밟아야 목적지에서 멈출 수 있을까?

 

V0로 움직이는 물체가 있다고 가정하자. 브레이크의 가속도는 a이다. V0으로 움직이는 물체가 속도가 0이 되기 위해서 걸리는 시간 t는 다음과 같다.

브레이크를 밟아야 하는 거리 r은 속도가 0이 될때까지 이동한 거리와 같으므로,

 

위 식이 성립한다. 여기에 안전하게 멈출 수 있도록 여분의 간격을 더하면 될 것이다.

 

public class ASObjectMovementFunc_Arrive : ASObjectMovementFunc
{
    /* ...전략 */
    public static readonly float TimesSlowThanStop = 2f;    //정지 반경에 비해 감속 반경이 몇 배 큰지
    public static readonly float MinStopRadius = 10f;	//이거 없으면 StopRadius가 너무 작음
    private float _stopRadius;
    private float _slowRadius;


    protected override SteeringOutput GetSteer()
    {
    /*...전략 */   
    
        //v속도로 가는애가 r만큼가서 멈췄다. (브레이크 가속도는 a)
        //이때 걸린시간 t= V/a
        //따라서 이동거리 r = (처음속도(=V) + 나중속도(=0))*1/2*t = V^2/(2*a)
        _stopRadius = _character.Velocity.sqrMagnitude * 0.5f / _target.BrakeDragAccel + MinStopRadius;  
        _slowRadius = TimesSlowThanStop * _stopRadius;
        
    /* 후략... */
    }
}

 

위에서 말한 규칙으로 정지 반경을 정했다.

 

 

그 결과

k=1.0
k=1.0

브레이크가 잘 동작하는 듯 하다. 

 

참고로 책에서는 StopRadius가 없어도 잘 동작한다고 한다. 그러나 낮은 프레임 레이트나 캐릭터가 높은 속도일때 오류가 있을 수 있어 넣은 대비책이라고 한다. 확실히 정지 반경을 동적으로 하기 전의 모습을 보면 StopRadius가 없는 경우 목표를 지나칠 수 있을 듯.

 

+) 

브레이크 밟을때 리턴해서 엑셀을 안 줬는데, 이렇게되면 방향을 트는게 느려질 수 있을 것 같아 리턴을 안 하도록 했다. 

 

k=1

확실히 코너링(?)이 좋아졌다.

 

참고 : Ian Millington, AI for GAMES 3rd edition, CRC press, [56~63p]

'Game AI' 카테고리의 다른 글

Game AI - Steering Behaviors (3)  (1) 2021.10.03
Game AI - Steering Behaviors (2)  (1) 2021.08.07
Game AI - Kinematic movement alogrithms  (2) 2021.06.13
게임 AI의 구현  (2) 2021.05.31
게임 AI의 모델  (0) 2021.05.24