Skip to main content

AI Engine update - Week ending 6/20

This week, I worked on rendering objects with direction and velocity. The agent is rendered as a triangular object, which points at its current heading. The green line represents the velocity of the object. The text underneath represents the speed.

I worked on additional behavior, which is the arrive behavior. Unfortunately I ran into a bug, where the velocity doesn't seem to be getting updated consistently. On further investigation, it turns out that the update loop was running so fast that the delta time between frames was turning up to be 0. What confuses me is that if the delta time is 0, then the agent shouldn't be moving at all, based on these lines:

Vec2 Acceleration = SteeringForce / mMass;

mVelocity += Acceleration * deltaTime;

mPosition += mVelocity;

On further debugging, I discovered that the delta time is non-zero for the first few updates (suggesting that there was perhaps some setup code that was still running when the game loop starts) - and then it becomes 0. This probably means that the agent reaches a certain initial velocity and then remains at that velocity.

Anyhow, this is how Arrive is implemented:


The problem with seek is that it maintains acceleration (or constant velocity if the max speed is reached) - all the way until it reaches the target. The problem with this is that it will tend to overshoot the target and bounce back and repeat this process. Even if the update loop is granular enough for arrive to detect reaching the target, the movement will not look graceful. Arrive allows for the agent to decelerate, or "ease" to a stop. To implement this, we define a radius inside which the deceleration will become active. If we are outside the radius, we simply Seek to the target. If we are inside it, the speed will be proportional to the distance from the target.

Vec2 Arrive::GetSteeringForce() const
       Vec2 force;

       Vec2 vecToTarget = mTargetPosition - mAgent.GetPosition();
       double distance  = vecToTarget.GetLength();

       if (distance > DefaultBrakeRadius)
              Vec2 direction = Vec2::Normalize(mTargetPosition - mAgent.GetPosition());
              Vec2 desiredVelocity = direction * mAgent.GetMaxSpeed();

              force += desiredVelocity - mAgent.GetVelocity();

       else if (distance > 0.0f)
              double speed = distance / mDecelerationFactor;

              speed = min(speed, mAgent.GetMaxSpeed());

              Vec2 desiredVelocity = Vec2::Normalize(vecToTarget) * speed;

              force += desiredVelocity - mAgent.GetVelocity();

       return force;

Behavior summing

A single agent can have multiple steering forces acting upon it. All the forces acting upon it should affect the agent simultaneously.

There are many ways to implement behavior summing. The simple, straightforward (albeit naïve) approach would be to simply add all the forces.
The approach I've chosen is to do a weighted sum: Each behavior is given a weight. Based on the weight given, the percentage of force is calculated. Then all these are added together to obtain the final force.

Dynamically changing goal positions and behaviors

I also now added functionality to help move the goal when the mouse button is clicked. The position where the mouse click occurs is sent to the world, and the world will update the agents' goals. I've also mapped keys - A, S and F, and the Shift + versions of those, to enable/disable those behaviors.

Next week

This week, I was to implement other steering behaviors such as Pursue and Evade, as well. However, as I mentioned above, I've ran into a lot of problems with the custom "engine" I've written - double precision floats and the game loop don't seem to be very precise and I'm having to delve down and fix those from time to time. I am also finding myself having to spend quality time on rendering and Win32-specific areas. While I love exploring new tech, and deeply enjoy new platforms to work on, it takes a lot of time and effort to write a good core game loop, and it seems like I am spending a majority of my time on that, rather that on the actual AI. I am seriously considering moving to a different engine, such as OGRE or Unreal, to ramp up the amount of time I spend on the actual AI implementation. That is, after all, the main goal of this project.

Since most of my code is written in a platform-agnostic manner, it should not take too much time to port this over to the engine of my choice. This is what I have planned for next week:
- Pick an engine to port the code over to. (2h)
- Port the code and make sure it works (5h)
- Finish pending behaviors (Evade, pursue, wander, arrive) (5h)
- Allow keyboard input to control one agent while other agents follow the one being controlled (3h)


Popular posts from this blog

AI Controller Update - Week ending 7/4

This week, I worked on Obstacle Avoidance and a slight hint of navigation. Obstacle Avoidance Though the concept is simple enough, the execution is quite tricky. The first part is to detect obstacles in your area. We define a lookahead, which can vary based on the object’s current velocity. I’ve left it at a fixed value for now. Then we raycast forwards to find the first obstacle in our path. Next, we compute the normal from the point of impact. We steer our object away from the point in the direction of the normal. The code looks like this: bool bHit = GetWorld ()-> LineTraceSingleByChannel ( Hit , StartLocation , EndLocation , Channel , QueryParams , ResponseParam );        if ( bHit )        {               FVector PenetratedAlongHit = Hit . ImpactPoint - EndLocation ;               FVector PenetratedAlongNormal = PenetratedAlongHit . ProjectOnToNormal ( Hit . ImpactNormal );               float PenetrationDepth = PenetratedAlongNormal . Size ();

AI Controller Update - Week ending 7/18

This was another landmark week. I was finally able to get navigation meshes working with all the behaviors I’d implemented so far. To see how important this is, let’s consider an example: The follower (the smaller model) needs to get to the target position (the other character in this case). However, there is an obstacle in between. The steering algorithm doesn’t know anything about obstacles. So it will continually push the follower towards the actor, and into the obstacle. With a navmesh, we can tell the steering algorithms to follow a sequence of points in order to reach the target actor. Now, calculating this every frame is very expensive. So what do we do here? First, we raycast to the target position to see whether the follower can reach there uninterrupted. If it can, fantastic. We just ask the follower to seek to the target position. If there isn’t a direct path, we request the navmesh to provide us with a set of points that we store, and then seek to one by one. Wh

AI Controller Update - Week Ending 6/27

Now, this was a week in which some really important work was done. Last week, I wrote about the roadblocks I was facing with writing my own game engine – guaranteeing a smooth game loop, constant frame rate, and float precision. While it is quite easy to build a functional game loop, it is harder to build one that does not break under stress. Somehow my windows framework wasn’t sending regular updates to my main loop, and I spent quite a while scratching my head trying to figure it out. However, time is precious and it was running out. I had to make a decision, and make it quick. I chose to jump into Unreal and port over all my code into Unreal 4.16. Jumping to Unreal I wanted to build a follower behavior, but I also wanted to build it right. So, I made sure to have a component-based architecture from the get-go, and made sure to have the steering behaviors as Actor Components, so that they could be attached to any actor and be reused. The Actor Steering component