ArgGrr gonna try Devtember

Day 19:

Added:
More zoms, just cloning one randomly through the maze n times.
Unitychan has health, zom bites remove health.
Made the zom walking animation look a bit better and then somehow made it worse in another way.

I should be defining interfaces for PCs, NPCs, objects and whatnot.
Then they will be able to interact with each other in a standard fashion.

I made some quick interfaces: Interface.cs

/// <summary>
/// 
/// </summary>
public enum life_status
{
    alive,
    dead
}

/// <summary>
/// Interface for people / characters in the game.
/// </summary>
public interface person
{
    /// <summary>
    /// Attack incoming to this character
    /// </summary>
    /// <param name="power">Power of incoming attack in HP</param>
    void attacked(int power, person Attacker);

    /// <summary>
    /// Get alive/dead status of this person / character.
    /// </summary>
    /// <returns></returns>
    life_status GetLifeStatus();
}

/// <summary>
/// Interface for interactable objects in the game.
/// </summary>
public interface thing
{
    //TODO: Put something in here?
}

Thing hasnā€™t been utilised yet. If I make some objects that PCs and NPCs can interact with.

What is the point of an interface? Every NPC should implement this interface, which means it must have the described set of functions. So for any NPC, we have a standard set of functions we can call on them to do stuff. In this case, we can attack them, and check if they are alive or dead.

In action: PC.cs

[RequireComponent(typeof(NavMeshAgent))]
[RequireComponent(typeof(Animator))]
public class PC : MonoBehaviour, person
{
    Animator anim;
    NavMeshAgent agent;
    Vector2 smoothDeltaPosition = Vector2.zero;
    Vector2 velocity = Vector2.zero;

    public float health = 100;
    public life_status Life = life_status.alive;

    void Start()
    {
        anim = GetComponent<Animator>();
        agent = GetComponent<NavMeshAgent>();
        // Donā€™t update position automatically
        agent.updatePosition = false;
    }

    void Update()
    {
        switch (Life)
        {
            case life_status.alive:

                Vector3 worldDeltaPosition = agent.nextPosition - transform.position;

                // Map 'worldDeltaPosition' to local space
                float dx = Vector3.Dot(transform.right, worldDeltaPosition);
                float dy = Vector3.Dot(transform.forward, worldDeltaPosition);
                Vector2 deltaPosition = new Vector2(dx, dy);

                // Low-pass filter the deltaMove
                float smooth = Mathf.Min(1.0f, Time.deltaTime / 0.15f);
                smoothDeltaPosition = Vector2.Lerp(smoothDeltaPosition, deltaPosition, smooth);

                // Update velocity if time advances
                if (Time.deltaTime > 1e-5f)
                    velocity = smoothDeltaPosition / Time.deltaTime;

                bool shouldMove = velocity.magnitude > 0.5f && agent.remainingDistance > agent.radius;

                // Update animation parameters
                anim.SetBool("move", shouldMove);
                anim.SetFloat("velx", velocity.x);
                anim.SetFloat("vely", velocity.y);

                GetComponent<Transform>().LookAt(agent.steeringTarget + transform.forward);

                break;
            case life_status.dead:
                //Do Nothing?
                break;
            default:
                break;
        }
    }

    void OnAnimatorMove()
    {
        switch (Life)
        {
            case life_status.alive:
                // Update position to agent position
                transform.position = agent.nextPosition;
                break;
            case life_status.dead:
                break;
            default:
                break;
        }
    }
    
    public void attacked(int power, person Attacker)
    {
        Debug.Log(this + " attacked by " + Attacker + " for " + power + " damage.");
        health -= power;
        if (health <= 0)
        {
            Life = life_status.dead;
        }
    }

    public life_status GetLifeStatus()
    {
        return Life;
    }
}

The functions mandated by the interface at the bottom.

Similarly: NPC.cs

[RequireComponent(typeof(NavMeshAgent))]
[RequireComponent(typeof(Animator))]
public class NPC : MonoBehaviour, person
{
    Animator anim;
    NavMeshAgent agent;
    Vector2 smoothDeltaPosition = Vector2.zero;
    Vector2 velocity = Vector2.zero;

    public PC nav_target;

    int health;
    bool dead;

    public int biteforce = 10;

    void Start()
    {
        anim = GetComponent<Animator>();
        agent = GetComponent<NavMeshAgent>();

        // Donā€™t update position automatically
        agent.updatePosition = false;
        // Do have nav agent update rotation to look down path
        agent.updateRotation = true;
    }

    Quaternion lastFrameRotation = Quaternion.identity;

    bool readytomove = false;
    bool bite = false;

    void Update()
    {
        Vector3 worldDeltaPosition = agent.nextPosition - transform.position;

        // Map 'worldDeltaPosition' to local space
        float dx = Vector3.Dot(transform.right, worldDeltaPosition);
        float dy = Vector3.Dot(transform.forward, worldDeltaPosition);
        Vector2 deltaPosition = new Vector2(dx, dy);

        // Low-pass filter the deltaMove
        float smooth = Mathf.Min(1.0f, Time.deltaTime / 0.15f);
        smoothDeltaPosition = Vector2.Lerp(smoothDeltaPosition, deltaPosition, smooth);

        // Update velocity if time advances
        if (Time.deltaTime > 1e-5f)
            velocity = smoothDeltaPosition / Time.deltaTime;

        Quaternion deltaRotation = transform.rotation * Quaternion.Inverse(lastFrameRotation);
        Vector3 eulerRotation = new Vector3(
            Mathf.DeltaAngle(0, Mathf.Round(deltaRotation.eulerAngles.x)),
            Mathf.DeltaAngle(0, Mathf.Round(deltaRotation.eulerAngles.y)),
            Mathf.DeltaAngle(0, Mathf.Round(deltaRotation.eulerAngles.z)));
        lastFrameRotation = transform.rotation;

        bool shouldMove = !agent.isStopped ;
        //bool shouldMove = velocity.magnitude > 0.5f && agent.remainingDistance > agent.radius && eulerRotation.magnitude > 0.5f;

        if (agent.remainingDistance < 0.5f)
        {
            Debug.Log(this + " is in range to attack " + nav_target);

            //Within range to attack
            if (nav_target.GetLifeStatus() == life_status.alive && !anim.GetBool("bite"))
            {
                //Notice that is will be called every frame.. Only do it when not in bite behavour?
                Debug.Log(this + " attacking " + nav_target + " for " + biteforce + " damage.");
                nav_target.attacked(biteforce, this);
                anim.SetBool("bite", true);
            }
        }

        // Update animation parameters
        anim.SetBool("move", shouldMove);
        anim.SetFloat("turn", eulerRotation.x);
        anim.SetFloat("fwd", velocity.y);

        GetComponent<Transform>().LookAt(agent.steeringTarget + transform.forward);

    }

    void OnAnimatorMove()
    {
        // Update position to agent position
        transform.position = agent.nextPosition;

        //Set destination once when agent hits the ground..

        if (nav_target.GetLifeStatus() == life_status.alive )
        {
            agent.SetDestination(nav_target.transform.position);
        }
    }

    public void attacked(int power, person Attacker)
    {
        Debug.Log(this + " attacked by " + Attacker + " for " + power + " damage.");

        if (health > 0)
        {
            //Modify hit power based on deflection, armour?
            // for now UnityChan squishy
            health -= power;
        }
        else
        {
            health = 0;
        }
    }

    public life_status GetLifeStatus()
    {
        if (dead)
            return life_status.dead;
        else
            return life_status.alive;
    }
}

Todo:
Zombie only attacks once. There is an issue where they can attack every frame, so I need to ensure they attack once, run out the animation, and then attack again.
Currently, they do not re-attack. Need to check over the code here. I am checking the state of the animator to check if they are in the bite animation state, and not biting if this is true. Maybe I need to auto-return from that state in the animator? Drat now I need to go check that.

6 Likes

Day 20:

Fixed my maze code. Turns out the top and left most rooms did not have down and right doors created, respectively. Here is the fixed snippet of code that creates the array of rooms and then creates & assigns walls to each room. It looks a bit tricky because the same wall should bound two rooms, so I donā€™t want to create two walls that are essentially the same thing. Because classes are reference types, assigning an instance of a class to different variables/properties just assigns a pointer to the same object in memory in the background. Structures on the other hand are value types, and copies are made when assigning them to different things.

   //Run back over array and create walls between all the rooms, putting in the references to rooms etc.
    for (y = 0; y < Maze_Segments_y; y++)
    {
        for (x = 0; x < Maze_Segments_x; x++)
        {
            // fixed some bugs here in map generation
            if (x == 0)
            {
                Rooms[x, y].Left = new Wall(null, Rooms[x, y]);
            }
            //Fixed a bug here, where first room was not getting a Right/Down wall assigned.
            if (x == Maze_Segments_x - 1)
            {
                Rooms[x, y].Right = new Wall(Rooms[x, y], null);
            }
            else
            {
                Wall tmpwall = new Wall(Rooms[x, y], Rooms[x + 1, y]);
                Rooms[x, y].Right = tmpwall;
                Rooms[x + 1, y].Left = tmpwall;
            }

            if (y == 0)
            {
                Rooms[x, y].Up = new Wall(null, Rooms[x, y]);
            }
            //Fixed a bug here, where first room was not getting a Right/Down wall assigned.
            if (y == Maze_Segments_y - 1)
            {
                Rooms[x, y].Down = new Wall(Rooms[x, y], null);
            }
            else
            {
                Rooms[x, y].Down = new Wall(Rooms[x, y], null);
                Wall tmpwall = new Wall(Rooms[x, y], Rooms[x, y + 1]);
                Rooms[x, y].Down = tmpwall;
                Rooms[x, y + 1].Up = tmpwall;
            }
        }
    }

Fixing this makes the generation a bit better. No more repeated patterns along these sides and a 2x2 cell in the top left corner. Also did a quick bit of code to debug.log the completed maze.

+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|     |  |                    |     |              |     |  |
+  +--+  +--+--+--+--+--+--+  +--+  +  +--+  +--+--+  +--+  +
|                                      |  |  |     |     |  |
+--+  +--+--+  +--+  +--+--+--+--+--+  +  +--+--+  +  +--+  +
|  |  |  |  |     |  |     |        |           |           |
+  +--+  +  +--+--+  +--+  +--+--+  +  +  +--+--+  +--+--+--+
|  |     |  |        |                 |  |     |           |
+  +  +--+  +--+--+--+--+--+  +  +--+  +--+  +--+  +--+--+--+
|     |  |  |     |  |  |     |     |     |  |  |        |  |
+  +--+  +  +--+  +  +  +  +--+--+--+--+  +  +  +  +--+--+  +
|     |        |  |  |  |  |        |           |  |  |     |
+  +--+--+--+  +  +  +  +--+--+--+  +--+--+--+  +  +  +  +--+
|                    |  |        |  |  |  |     |           |
+--+  +--+--+--+  +--+  +--+  +--+  +  +  +  +--+  +--+--+  +
|        |           |           |     |  |     |        |  |
+--+--+--+  +--+  +--+--+--+--+  +--+  +  +  +--+  +--+--+  +
|           |     |     |                       |        |  |
+--+--+--+  +--+  +--+  +--+  +--+--+--+--+--+  +  +--+--+  +
|           |  |  |  |  |     |     |  |  |  |  |        |  |
+  +--+--+  +  +  +  +  +--+--+--+  +  +  +  +  +  +--+--+--+
|  |     |  |        |     |  |        |           |  |     |
+  +  +  +--+  +--+  +  +  +  +--+--+  +--+  +--+--+  +--+  +
|  |  |        |  |     |  |        |           |  |        |
+  +--+  +--+--+  +--+  +--+  +--+  +--+--+--+  +  +--+  +--+
|  |     |  |              |  |        |     |     |     |  |
+--+--+  +  +--+--+--+--+  +--+  +--+  +--+  +  +--+--+  +  +
|        |                       |  |           |     |  |  |
+--+--+--+--+--+--+--+  +  +--+--+  +--+--+--+  +--+  +  +  +
|           |           |     |     |              |        |
+--+--+--+  +--+--+--+  +  +--+--+  +--+--+--+  +  +--+  +--+
|        |           |  |     |     |           |           |
+--+  +--+--+--+  +--+--+--+--+--+  +  +  +  +  +  +  +  +  +
|        |           |                 |  |  |  |  |  |  |  |
+--+--+  +--+--+--+  +  +--+  +--+  +--+  +  +  +--+--+  +--+
|                    |  |        |  |     |  |     |        |
+  +  +--+--+--+--+  +  +--+--+--+--+  +  +--+  +  +  +  +  +
|  |  |                       |        |  |     |  |  |  |  |
+--+--+--+--+--+  +  +  +  +--+--+--+  +--+  +--+  +  +  +  +
|                 |  |  |  |              |  |     |  |  |  |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

Toying a bit more with basic behaviour. You can assign a script to an animation/state machine cell. This contains a set of events that trigger at the start, end and during an animation. I can use this to turn off a parameter for an animation. So calling setbool(ā€œbiteā€,true) on the animator will start the bite animation, and cause the transition from walk / idle. When this animation finishes, the script will call setbool(ā€œbiteā€,false) meaning the bite will not automatically start again. I can also monitor this parameter from the NPC code etc.

That seems to work fine. Now I just need to sort out the NPC code, to make sure attacks are called in a fair fashion. E.g. zombies had an infinite range bite they that would happen when they were recalculating the path to the target. During this state, the range to the target can drop to 0. So using the range to target purely as a condition for the attack is not good.
Also, when they are first spawned in, they do an insta-bite from anywhere on the map. Need to figure this out.

3 Likes

Day 21:

Trying to fine tune the animations a bit more, and try and make them common between characters. I.e. same parameter names, same basic actions.

This state machine thing seems to be getting more complex. I should probably look and see how things are meant to be done. Now work is done for the year (bar being on call) I can spend a bit more time watching tutorials instead of mucking about.
But basically, the idle is the default animation. If you are walking, it transitions to the walk animation blend tree. If you do other actions, there are branches from other animations so you can do them at pretty much any time. Save with damage/death animations. Some animations need the behavior script to unset the boolean parameter that I use to trigger them, such as damage and attack states.

Now when a Zom attacks Unity-Chan, there is a damage reaction that happens. Unity-Chan also has an attack animation of her own now, but itā€™s just an animation at this stage. Need to figure out how to pick targets to receive damage for this one.
It is different to the Zoms, who have one known target.

image
Spinning animation that serves as an attack.

Thatā€™s about it for this update, not much new code.

6 Likes

Day 22:

Not much today. Watched some tutorials on YouTube, seen a bunch of stuff I sort of already know.
I also seen some official Unity videos showing off Ai Behavior Tree Visual Designer plugins for Unity, for sale for $40 from the store. I donā€™t want to buy one, so have been trying to figure out how to do my own. But it is really messy, I am juggling like three sets of states on some characters.

The basis is using an enumerator like this:

/// <summary>
/// Might be used to track state of animator state machine thing.
/// </summary>
public enum ai_status
{
    move,
    run,
    idle,
    rest,
    damage,
    massive_damage,
    attack,
    roaming,
    searching
}

Then in the script for a character, I can have a big switch character that does the required stuff each frame.

//Branch based on current action
switch (AI_Status)
{
	case ai_status.move:
		break;
	case ai_status.run:
		break;
	case ai_status.idle:
		//Check if not idling
		if (!anim.GetBool("idle")) anim.SetBool("idle", true);

		int r = Random.Range(0, 1000);
		//Consider resting randomly.
		switch (r)
		{
			case 1: case 2: case 3:
				anim.SetInteger("rest", r);
				anim.SetBool("idle", false);
				AI_Status = ai_status.rest;
				break;
			default:
				break;
		}
		break;
	case ai_status.rest:
		break;
	case ai_status.damage:
		break;
	case ai_status.massive_damage:
		//Should this be a substate of damage? Like the 3 rest states?
		break;
	case ai_status.attack:
		break;
	case ai_status.roaming:
		//Wandering around?
		break;
	case ai_status.searching:
		//Look for a known target?
		break;
	default:
		break;
}

The issue I need to iron out is how to properly manage the state for a character. It should be done with this. When the state changes, or I change it, this should force the animator into the same state. The behavior scripts could then talk directly back to the characters and set their state back to normal after an animation has finished?

I also need to figure out how to make a character see something. I could probably put a cone or something on their face, and when something collides with this cone, do a ray cast to it to make sure it isnā€™t obscured?
Might not work for things partially obscured. Need to look this stuff up.

3 Likes

Have you thought about extending your states to give them behaviors? By adding a couple of interfaces you could create state objects for each type of state that would know which animations to run for that state and by having your game entities implement a specific interface the states could communicate with the entities so the entity would be aware of the state changes (either when they are started/stopped manually or where they are timed states and the states themselves end after their animation is run.

Itā€™s been a while since I did any C# (or Mono) so please excuse the example pseudo code below (itā€™s probably a mix of C++ and C# :smile:)

enum State {
    Walk,
    Run,
    Attack,
    Damaged
};

//
// Common interface for all states
// This can be extended if more complex states are required
//

interface EntityState {
    
    State type();
    void enter();
    void leave();
};

//
// The game entity base class would implement this interface so the state can communicate with the entity
//

interface StatefulEntity {
    void enterState(EntityState state); 
    void leaveState(EntityState state);
};

class PlayerWalkState {
    private _entity;
    
    PlayerWalkState(StatefulEntity entity) {
        _entity = entity;    
    }
    
    State type() { return Walk; }
    
    void enter() {
        //
        // Start animation for the walk state here
        // then tell the entity the state has started
        //

       // <State specific code here>

        _entity.enterState(this);
    }
    
    void leave() {
        //
        // Stop the animation for the walk state here
        // then tell the entity the state has finished
        //
       
       // <State specific code here>

        _entity.exitState(this);    
    }
};


//
// Example of an entity implementing the StatefulEntity interface
//
    
class PlayerEntity : StatefulEntity {
    State _state;    // The entity now holds an instance of one of the State classes instead of a state enum
    
    PlayerEntity() {
        // EDIT: I forgot, it's important that the state gets a reference to the entity
        _state = new PlayerIdleState(this);       // For example, the player starts in the idle state so _state is an instance of "PlayerIdleState"
        _state.enter();
    }
    
    void enterState(State newState) {

        // There's probably not going to be much in here but it might be useful if you want to do anything
        // specific  when the entity transitions into a new state

        switch(newState.type()) {
            case Idle:  { /* Do Stuff for the idle state */} break;
            case Walk:  { /* Do stuff */ } break;
            ...
        }
    }
    
    void exitState(State oldState) {

        // This is where you would have logic to decide what the next state should be based on entity type, previous state and player input

        switch(oldState.type()) {
            case Idle: { 
                // Decide what the next state is and assign an instance of the object
                // to the entities "_state" member
            } break;
            
            case Walk:  {
            }
            break;
        }
        
        // If the state has changed then start the new state
        if(_state != oldState)
            _state.start();
    }
};

Itā€™s looks a little complicated but I think something along these lines makes it easier to add more states and more complex behaviours going forward. It also keeps the details of all the different state animations and mechanics out of the main Entity class.

Anyway, just some ideas that might be useful.

Shecks

5 Likes

Yes I think this is probably the way to go. I did some reading after my last post and realised I was trying to make a Finite State Machine. The example went down the design pattern route due to inherent limitations in my approach. I will give this a go and post an update!

2 Likes

Itā€™s Day 23:

First bits, importing your code. Next step is implement the interfaces in a character, probably the NPC one since they are meant to be fully autonomous.

So Iā€™ll add the name space, figure out if I need to attach the behaviors to the characters so they can access the animators to call the animations. Probably easy enough to do, but I need to do it in a common way.

Then I can build a tree so to speak, something that handles the transition between the different behaviors. I think it will work pretty well. Thanks @Shecks for the head start!

ShecksStateMachine.cs:

namespace ShecksStateMachine
{
    /// <summary>
    ///     Behaviors
    /// </summary>
    enum State
    {
        Idle,
        Rest,
        Walk,
        Run,
        Damage,
        Attack,
        Roaming,
        Searching
    }

    /// <summary>
    ///     Common interface for all states
    ///     This can be extended if more complex states are required
    /// </summary>
    interface iEntityState
    {
        /// <summary>
        ///     The behavior of this entity state.
        /// </summary>
        /// <returns></returns>
        State type();

        /// <summary>
        ///     Event, called when this behavior start?
        /// </summary>
        void enter();

        /// <summary>
        ///     Event, called when this behavior ends?
        /// </summary>
        void leave();
    }


    /// <summary>
    ///     The game entity base class would implement this interface so the state can communicate with the entity
    /// </summary>
    interface iStatefulEntity
    {
        /// <summary>
        ///     Event, called when the entity transitions to a behavior
        /// </summary>
        /// <param name="state"></param>
        void enterState(iEntityState state);

        /// <summary>
        ///     Event, called when the entity transitions to a new behavior
        /// </summary>
        /// <param name="state"></param>
        void leaveState(iEntityState state);
    }
}

behaviors.cs:

namespace ShecksStateMachine
{

    /*
        Idle
        Rest
        Walk
        Run
        Damage
        Attack
        Roaming
        Searching

     */

    class PlayerIdleState : iEntityState
    {
        private iStatefulEntity _entity;

        public PlayerIdleState(iStatefulEntity entity)
        {
            _entity = entity;
        }

        public State type() { return State.Idle; }

        public void enter()
        {
            //
            // Start animation for the walk state here
            // then tell the entity the state has started
            //

            // <State specific code here>

            _entity.enterState(this);
        }

        public void leave()
        {
            //
            // Stop the animation for the walk state here
            // then tell the entity the state has finished
            //

            // <State specific code here>

            _entity.leaveState(this);
        }

    };
    class PlayerRestState : iEntityState
    {
        private iStatefulEntity _entity;

        public PlayerRestState(iStatefulEntity entity)
        {
            _entity = entity;
        }

        public State type() { return State.Idle; }

        public void enter()
        {
            //
            // Start animation for the walk state here
            // then tell the entity the state has started
            //

            // <State specific code here>

            _entity.enterState(this);
        }

        public void leave()
        {
            //
            // Stop the animation for the walk state here
            // then tell the entity the state has finished
            //

            // <State specific code here>

            _entity.leaveState(this);
        }

    };
// snip

}

The main issue I had here was, how do I trigger animations from the PlayerRestState class, for example.
My solution is to to grab references to the character Animator and NavMeshAgent and store them in the behavior objects.

/// <summary>
///     The game entity base class would implement this interface so the state can communicate with the entity
/// </summary>
public interface iStatefulEntity
{
//snip
    /// <summary>
    /// Get the Animator used by the Character so I can call animations
    /// </summary>
    /// <returns>Animator used by the Character</returns>
    Animator getAnimator();

    /// <summary>
    /// Get the NavMeshAgent used by the Character so I can get it to navigate.
    /// </summary>
    /// <returns>NavMeshAgent used by the Character</returns>
    NavMeshAgent getNavAgent();
}

That is the interface, here is a behavior object:

    public void enter()
    {
        //
        // Start animation for the walk state here
        // then tell the entity the state has started
        //


        Debug.Log("Entering PlayerIdleState");
        _animator.SetBool("idle", true);

        // <State specific code here>

        _entity.enterState(this);
    }

    public void leave()
    {
        //
        // Stop the animation for the walk state here
        // then tell the entity the state has finished
        //
        Debug.Log("Leaving PlayerIdleState");
        _animator.SetBool("idle", false);

        // <State specific code here>

        _entity.leaveState(this);
    }

};

The object orientated stuff is complex and hard to get your head around, but itā€™s all about building objects that do the work for you. So once I get these behavior objects built and with some basic functionality, I can start stringing them together.

4 Likes

Day 24:

Ugh this isnā€™t working for me.

Been going back and forward on the state machine stuff and itā€™s not making much sense to me. I get the parts, but putting it together I keep hitting a road block. Where do I put character code? In the behaviors or the character classes? How usable are the behaviors for other characters? It isnā€™t jelling inside my head with how I think the stuff should work.
I need to dial it back a bit and come back to the state machine stuff in the future.

Also, there is already a pretty solid state machine visual designer built in, the whole animation controller thing. By building an additional state machine, I am doubling up on a fair bit of work. There are drawbacks to putting too much game code into the animation controller, just makes debugging and finding out where things happen a bit harder. But shouldnā€™t matter at this stage.

Also investing the use of sub state machines, so keep things a bit simpler.

Latest iteration of Unity-Chan Animation Controller.
Rather than having a separate rest state, I can incorporate it into the idle state.

In side the idle state, it will more or less pick a random animation. There is some weighting controlled by a behavior script. Only issue now is how to interrupt these animations when something else is called.

override public void OnStateMachineEnter(Animator animator, int stateMachinePathHash)
{
    int animation_number = Random.Range(0, 10);
    animator.SetInteger("idle_animation", animation_number);
}

Here as soon as the sub state is entered, the parameter is set to a random number. A transition will play dependent on this number.

Also started using trigger parameters, instead of boolean/integer ones for some things. For an attack animation, I just want to trigger it and have it play out with no interruptions. I can save time by not having to un-set the boolean parameter when it is done. But maybe it is also a bad idea.

4 Likes

Pagan Holiday:

Wut, Christmas update?

Iā€™m heading back to the finite state machine. I need to build up to the full design pattern object orientated behavior stuff.
I have a property I use to track the current state of the actors. When you set it, it follows through and disables the previous animation and enables the new one. Thatā€™s the theory anyway, still tricky to muster.

ai_status _status;
public ai_status status
{
    get { return _status; }
    set
    {
        //UnityEngine.Debug.DebugBreak(); 
        //No change
        if (value == _status) return;

        //End old state
        switch (_status)
        {
            case ai_status.move:
                _animator.SetBool("move", false);
                break;
            case ai_status.idle:
                _animator.SetBool("idle", false);
                break;
            case ai_status.damage:
                break;
            case ai_status.attack:
                break;
            case ai_status.roaming:
                break;
            case ai_status.searching:
                break;
            case ai_status.dead:
                //Cant leave death
                break;
            default:
                break;
        }
        // Start new state
        switch (value)
        {
            case ai_status.move:
                _animator.SetBool("move", true);
                break;
            case ai_status.idle:
                _animator.SetBool("idle", true);
                break;
            case ai_status.damage:
                _animator.SetTrigger("damage");
                break;
            case ai_status.attack:
                _animator.SetTrigger("attack");
                break;
            case ai_status.roaming:
                break;
            case ai_status.searching:
                break;
            case ai_status.dead:
                _animator.SetTrigger("dead");
                break;
            default:
                break;
        }
        _status = value;
    }
}

Also playing with some code to check if NPC can see the player:

    bool rayhit, angleok;
    //Do a raycast to the target
    RaycastHit hitInfo = new RaycastHit();
    Ray ray = new Ray(this.transform.position, target.getTransform().position  - this.transform.position);

    angleok = Vector3.Angle(this.transform.position - target.getTransform().position, this.transform.forward) > 135; //180 = fwd, ((180-135)*2) = 90 

    Physics.Raycast(ray.origin, ray.direction, out hitInfo);
    rayhit = hitInfo.transform == target.getTransform();

    if (rayhit)
    {
        //0 deg is behind, 180 is forward!!
        UnityEngine.Debug.DrawRay(this.transform.position, target.getTransform().position - this.transform.position, Color.red, 30f);
        UnityEngine.Debug.DrawRay(this.transform.position, transform.forward , Color.green, 30f);
        UnityEngine.Debug.Log("Can see direction: " + Vector3.Angle(this.transform.position - target.getTransform().position, this.transform.forward));
    }

    return rayhit && angleok;

There are some debug drawing of rays so I can check things are working right.
Itā€™s all still a mess. It might be better by the end of the month!

6 Likes

Dec 26:

More hammering in some code getting things working. Now, zombies default to a searching state. They will wander around, or move to random locations, and If they get direct line of sight on the player, they will enter a following state, where they will follow the player and attack when they get close.

Red lines show a line of sight on the target. Green is their forward line at the time.

Wall of code following.

[RequireComponent(typeof(NavMeshAgent))]
[RequireComponent(typeof(Animator))]
public class NPC : MonoBehaviour, iPerson
{
    Animator _animator;
    NavMeshAgent _navagent;
    Vector2 smoothDeltaPosition = Vector2.zero;
    Vector2 velocity = Vector2.zero;
    Transform _transform;

    public PC nav_target;

    GameObject PlayerObject;

    int health;
    bool dead;
    Quaternion lastFrameRotation = Quaternion.identity;

    bool readytomove = false;
    bool bite = false;


    public int biteforce = 10;

    ai_status _status;
    public ai_status status
    {
        get { return _status; }
        set
        {
            //No change
            if (value == _status) return;

            //End old state
            switch (_status)
            {
                case ai_status.move:
                    _animator.SetBool("move", false);
                    break;
                case ai_status.idle:
                    _animator.SetBool("idle", false);
                    break;
                case ai_status.damage:
                    break;
                case ai_status.attack:
                    break;
                case ai_status.roaming:
                    //No anim state to change
                    _animator.SetBool("move", false);
                    break;
                case ai_status.searching:
                    //No anim state to change
                    _animator.SetBool("move", false);
                    break;
                case ai_status.dead:
                    //Cant leave death
                    _animator.SetBool("idle", false);
                    break;
                case ai_status.follow:
                    _animator.SetBool("move", false);
                    break;
                default:
                    break;
            }
            // Start new state
            switch (value)
            {
                case ai_status.move:
                    _animator.SetBool("move", true);
                    break;
                case ai_status.idle:
                    _animator.SetBool("idle", true);
                    break;
                case ai_status.damage:
                    _animator.SetTrigger("damage");
                    break;
                case ai_status.attack:
                    _animator.SetTrigger("attack");
                    break;
                case ai_status.roaming:
                    _animator.SetBool("move", true);
                    break;
                case ai_status.searching:
                    _animator.SetBool("move", true);
                    break;
                case ai_status.dead:
                    _animator.SetTrigger("dead");
                    break;
                case ai_status.follow:
                    _animator.SetBool("move", true);
                    break;
                default:
                    break;
            }
            _status = value;
        }

        
    }

    void Start()
    {
        //Store references to things so we dont have to keep digging them up.
        _animator = GetComponent<Animator>();
        _navagent = GetComponent<NavMeshAgent>();
        _transform = GetComponent<Transform>();

        status = ai_status.searching;

        PlayerObject = GameObject.FindGameObjectWithTag("Player");
    }

    void Update()
    {

        if (_animator.GetBool("move"))
        {
            move_along_navpath();
        }

        switch (status)
        {
            case ai_status.move:
                break;
            case ai_status.idle:
                break;
            case ai_status.damage:
                break;
            case ai_status.attack:
                break;
            case ai_status.roaming:
                if (!_navagent.hasPath)
                    //Walk to random position.
                    _navagent.SetDestination(this.transform.position + new Vector3(Random.Range(-2, 3), 0, Random.Range(-2, 3)));
                break;
            case ai_status.searching:

                if (!_navagent.hasPath)
                    _navagent.SetDestination(this.transform.position + new Vector3(Random.Range(-2, 3), 0, Random.Range(-2, 3)));
                if (canSeePlayer())
                {
                    //Can see target, start following
                    status = ai_status.follow;
                }
                break;
            case ai_status.follow:
                _navagent.SetDestination(PlayerObject.transform.position);
                
                if (_navagent.hasPath && (_navagent.remainingDistance < 0.5f))
                {
                    //Stop character moving when at or very close to target.
                    _navagent.isStopped = true;
                    status = ai_status.idle;
                }
                else if (_navagent.isStopped  && (_navagent.remainingDistance >= 0.5f))
                {
                    //Start moving again if target moves.
                    _navagent.isStopped = false;
                    status = ai_status.move;
                }

                if (_navagent.hasPath && (_navagent.remainingDistance < 1f && _navagent.remainingDistance > 0.4f))
                {
                    //Attack!! If close, has a target and valid path. But not a zero distance because invalid paths have 0 distance.
                    status = ai_status.attack;
                    status = ai_status.follow;
                }
                break;
            default:
                break;
        }
    }


    void move_along_navpath()
    {
        //Have a path, move the character manually along it, and also face the right way.
        Vector3 worldDeltaPosition = _navagent.nextPosition - _transform.position;

        // Map 'worldDeltaPosition' to local space
        float dx = Vector3.Dot(_transform.right, worldDeltaPosition);
        float dy = Vector3.Dot(_transform.forward, worldDeltaPosition);
        Vector2 deltaPosition = new Vector2(dx, dy);

        // Low-pass filter the deltaMove
        float smooth = Mathf.Min(1.0f, Time.deltaTime / 0.15f);
        smoothDeltaPosition = Vector2.Lerp(smoothDeltaPosition, deltaPosition, smooth);

        // Update velocity if time advances
        if (Time.deltaTime > 1e-5f)
            velocity = smoothDeltaPosition / Time.deltaTime;

        // Update animation parameters
        _animator.SetFloat("velx", velocity.x);
        _animator.SetFloat("vely", velocity.y);

        GetComponent<Transform>().LookAt(_navagent.steeringTarget + transform.forward);

    }

    void OnAnimatorMove()
    {
    }

    public void attacked(int power, iPerson Attacker)
    {
        if (health > 0)
        {
            //Modify hit power based on deflection, armour?
            // for now UnityChan squishy
            health -= power;
        }
        else
        {
            health = 0;
        }
    }

    public life_status GetLifeStatus()
    {
        if (dead)
            return life_status.dead;
        else
            return life_status.alive;
    }


    public Animator getAnimator() {return _animator;}
    public NavMeshAgent getNavAgent() {return _navagent;}


    public bool canSee(GameObject target)
    {
        bool rayhit, angleok;
        //Do a raycast to the target
        RaycastHit hitInfo = new RaycastHit();
        Ray ray = new Ray(this.transform.position, target.transform.position  - this.transform.position);
        //Try get the angle from one character to another.
        angleok = Vector3.Angle(this.transform.position - target.transform.position, this.transform.forward) > 135; //180 = fwd, ((180-135)*2) = 90 
        //Follow the ray, see what it hits.
        Physics.Raycast(ray.origin, ray.direction, out hitInfo);
        rayhit = hitInfo.transform == target.transform;

        return rayhit && angleok;
    }

    public bool canSeePlayer()
    {
        //No doubt this is super inefficient.
        bool rayhit, angleok;
        //Do a raycast to the target
        RaycastHit hitInfo = new RaycastHit();
        Ray ray = new Ray(this.transform.position, PlayerObject.transform.position - this.transform.position);

        angleok = Vector3.Angle(this.transform.position - PlayerObject.transform.position, this.transform.forward) > 135; //180 = fwd, ((180-135)*2) = 90 

        Physics.Raycast(ray.origin, ray.direction, out hitInfo);
        rayhit = hitInfo.transform == PlayerObject.transform;

        if (rayhit)
        {
            //0 deg is behind, 180 is forward!!
            UnityEngine.Debug.DrawRay(this.transform.position, PlayerObject.transform.position - this.transform.position, Color.red, 30f);
            UnityEngine.Debug.DrawRay(this.transform.position, transform.forward, Color.green, 30f);
            UnityEngine.Debug.Log("Can see direction: " + Vector3.Angle(this.transform.position - PlayerObject.transform.position, this.transform.forward));
        }

        return rayhit && angleok;
    }


    public Transform getTransform()
    {
        return this.transform;
    }
}

I guess my style of doing this sort of programming is get all the stuff figured out and then work out a way to consolidate it. Most of this is duplicated between the PC and NPC classes.

4 Likes

Day Because I love nothing more than trying new things and commenting out a bunch of stuff I already did and love the bomb:

I had a look at an excellent Unity tutorial on state machines. This one is more inheritance based, but allows you to set up a bunch of Action, State and Decision scripts and then design Finite State Machines in the editor by chaining them together. It seems a good approach.

It builds on a prefabbed tank game, but I built it into my dumb map zombie survival horror test game.
It seems you can build it into anything, taking note there are a couple of scripts that it relies on that you need to dig out of the tank base game. Mainly the class that holds stats about the enemies, and one that handles attacking. Also, it seems to have a good way to reference the entities from within the scripts, something I struggled to deal with in other types of state machines. A statemachine script is added to a prefab, and this is passed down through the behavior tree to all the Actions, Decisions and States, exposing the top level entity as required. This seems to work for me.

So following along the tutorial (up to 8/10), I have the zombies starting in a Patrolling mode, walking between 4 waypoints in the maze. (Each corner, I set these after generating the maze):

If they see the player, they transition to a Chase state, where they move directly to the player.
If they are in range for an attack, they will do so until the player is dead. At which point they return to patrolling mode:

The gizmo colour is set on the mesh sphere around each zom so you can see in the scene view what state they are in.

Seems similar to what I had before, but less clunky and pieced together in the UI.

So what did we end up with so far? Zoms patrolling. First waypoint bottom left where UnityChan is idling.
Most of the first bunch close see her and transition to chasing/attacking. Later ones donā€™t see her due to the moshpit of zoms, and continue patrolling to the next waypoint.

That was a fair chunk of work today, also including a whole bunch more stuff on movement of the characters. I had two different ways set up Root Motion for the Zoms, meaning the movement of the entity is derived from the animation. Since they have a lumbering zombie walk that isnā€™t consistent in speed, it made sense to try and get this working. it worked, but they move really slow.
And then for UniChan, having the movement determined by the NavAgent, since all her animations were static ones. I.e. ones that did not have any root motion. So if you play the animation, the character appears to run on the spot.

3 Likes

Didnā€™t do too much, other than make a new state/action set for random patrolling instead of waypoint patrolling. All these actions, states and decisions build on their respective base classes. The base abstract is sort of a template. Then you make the ones with the specific behaviors you want. Then, in the editor you can create the actual things that will be used in the game. Seems like 3 layers deep, but it sort of works I guess.

here is the base template thing for all actions:

public abstract class Action : ScriptableObject
{
    public abstract void Act(StateController controller);
}

Here is my random patrolling action building on that, and available in the editor:

[CreateAssetMenu(menuName = "PluggableAI/Actions/Random Patrol")]
public class PatrolRandomAction : Action {

    public override void Act(StateController controller)
    {
        Patrol(controller);
    }

    private void Patrol(StateController controller)
    {
        controller.navMeshAgent.destination = controller.CurrentRandomPatrolTarget;
        controller.navMeshAgent.isStopped = false;

        if (controller.navMeshAgent.remainingDistance <= controller.navMeshAgent.stoppingDistance && !controller.navMeshAgent.pathPending)
        {
            controller.CurrentRandomPatrolTarget = controller.transform.position + new Vector3(Random.Range(-3, 4), 0, Random.Range(-3, 4));
        }
    }
}

Now you can instantiate or create instances of this action script in the editor like so:


Notice the new menu made by the class parameters.

An action isnā€™t useful by itself. It needs to be loaded into a state.
image
If there are decisions attached, it will also loop through these, and transition to a new state if required. The RemainInState just means stay in the same state as you would guess.

So to add a new random patrol, I had to duplicate and rename the regular patrol states, and just change the action to the new one.

I also had to make a few tweaks to the state controller, the monobehavior object that is attached to the zombie prefab. I just put in a field to store the current random location to travel too. This is assigned an initial value in the scene script, and when the agents reach the random point, a new one is generated on the fly by the above action script. It seems to work. I imagine the StateController object will have a large number of fields as the game gets more complex.

5 Likes

Day 29:

Have a quick play late at night to see if I can add a new state, behavior.

When the enemies are chasing the player, if they lose sight they should, instead of magically tracking through the maze, run to last known position, have a look around, and continue chasing if seen. If not, patrol around or whatever.

This means I need to make a new variable in the state controller, to store the last seen/known position of the player. This will be updated every time they do a spherecast to see if they can see the player. This is usually done when patrolling, chasing and attacking the player.

A sphere cast is like a ray cast, but it sends a sphere down a path infront of the character to see what they collides with. Like a thicc ray.

//Update last known position
controller.Last_Known_Position = hit.transform.position;

Since the state controller is bound to each instance of an enemy, they all store their own last known positions.

Now my action that will run the enemy to the last known position:

[CreateAssetMenu(menuName = "PluggableAI/Actions/Run to Last Known Position")]
public class Action_Run_to_last_known_position : Action {
    public override void Act(StateController controller)
    {
        Chase(controller);
    }

    private void Chase(StateController controller)
    {

        if (controller.Last_Known_Position == Vector3.zero)
        {
            //Whoops no last known position.
            //should make a decision to check for a last known position and change state if not.
            controller.navMeshAgent.isStopped = true;
        }

        controller.navMeshAgent.destination = controller.Last_Known_Position;
        controller.navMeshAgent.isStopped = false;
    }
}

I need a couple of decisions. One to ensure there is a last known postion, and one to determine if I am at the last known position when running at it. Maybe some of this could be rolled into the action, more work to be done. I.e. in the action, check to see if I get there.

[CreateAssetMenu(menuName = "PluggableAI/Decisions/Am I At Last Known Position?")]
public class Decision_Am_I_At_Last_Known_Position : Decision  {

    public override bool Decide(StateController controller)
    {
        //Maybe need to be a bit more precise, incase cant get to the last known location or whatevs
        return controller.navMeshAgent.isStopped;   
    }
}

Then set up the state a bit like this:

So what do you get for all this?


It is half working and half broken.
The transition to the ā€œrun to last known positionā€ is going fine, but they either get there or not and start scanning endlessly. Need to do more work. There is some extra code to draw a gizmo at the last known location position, and a line linking the enemy to that position. Might not be that easy to see in that screenshot.

I also started renaming all of the states and actions so they make more sense. The naming convention from the videos were confusing me much.

If nothing else I am learning stuff.

4 Likes

Prepare the ban hammer:
So innocently doing some programmingā€¦ Making a new action for a look left & rightā€¦

[CreateAssetMenu(menuName = "PluggableAI/Actions/Look left & right")]
public class Action_Look_Around : Action
{
    public override void Act(StateController controller)
    {
        //Try do a complex look left, right then back to normal.

And this happens! I forgot about the & thingā€¦

3 Likes

Dec 30:

Mainly debugging the state machine. I have determined that the zoms/emenies are blind as a bat, lose visual contact with player very easily. Even when standing right in front of them. Probably need to figure out a better way to do the ā€œcan seeā€ thing.

I am thinking perhaps, a cone or pyramid shape from the eyes pointing forward. This would clip through all the terrain, but if it collides with an enemy, then do a sphere cast from the eye location to the enemy location to ensure they can be seen. The whole issue is peripheral vision.
Maybe tomorrow I can get cracking on that.

image
UnityChan also knows the first rule of not being seen perhaps.

2 Likes

Day ā•“ā•Øā•–:

FFS I spent an hour trying to get a collider to trigger when a rigid body enters it.
It isnā€™t working. Ill get back to it later.

Ditched that idea. Seems expensive to throw cubes around and check for collisions. I tried another alternate method, which creates a sphere and returns everything that collides with that. Also expensive I think. You need to play with layer masks, or its returning thousands of things which need to be sorted through every frame it is happening.

I went back to the first sort of idea, raycast directly to the player, check it is in the field of vision, then do a sphere cast to make sure they can be seen. Still runs into issues where it misses when standing right in front of the player. Odd.

Then Charlie Brooker went and put Bandersnatch of NetFlix. And of course I heard the bit at the end. So you can bet your arse I am gonna rip the audio and put it into an emulator. He was using a ZX Spectrum I think? I never used one before, so that is what I am doing now. Not like I am in control.

2 Likes

Def something there :wink:

3 Likes

For those interested, there is more in the bandersnatch demo than the Black Mirror test pattern, I just didnā€™t want to spoil it.

Ah looks like I solved the cant see unity chan issueā€¦

The code is big and messy, but can now be trimmed down since it is working much better.

Couple of issues solved. I think the sphere cast was hitting the casting object, say the enemy. I had to use the layer mask bit to ignore all enemies. This adds the advantage of having enemies see UnityChan through other Enemies. I am fine with that. Imagine the case where several enemies have mobbed UnityChan, and another Enemy runs past, thinking ā€œnothing strange going on hereā€.

Second issue is probably the sphere cast was hitting the ground. If UnityChanā€™s origin is at her feet, then all sphere casts at her position will dive into the ground and collide with it, reducing the effective range.
I also made sure to set all sub assets of UnityChan to be tagged with ā€œPlayerā€, so a collision with any part counts.

There is probably more, but that is enough for now.

private bool Look(StateController controller)
{

    RaycastHit hit;

    //Cache player position. It is cheating knowing this, but only used to determine if in line of sight.
    Transform playertx = GameObject.FindGameObjectWithTag("Player").transform;
    Vector3 leveldirection = playertx.position - controller.eyes.position;

    //Get FOV angle
    Vector3 reletiveNormalizedPos = (leveldirection).normalized;
    float dot = Vector3.Dot(reletiveNormalizedPos, controller.eyes.forward);

    //Try and get a level (to the ground) vector from the enemy to the player, to prevent premature collisions with ground? Seems to help a bunch.
    leveldirection.y = 0;

    //angle difference between looking direction and direction to item (radians)
    float angle = Mathf.Acos(dot) * Mathf.Rad2Deg;

    if (angle < 90)
    {
        //looking at target

        //
        //if (Physics.Raycast(controller.eyes.position, playertx.position - controller.eyes.position, out hit, controller.enemyStats.lookRange, ~(1 << 9), QueryTriggerInteraction.UseGlobal)
        if (Physics.SphereCast(controller.eyes.position, controller.enemyStats.lookSphereCastRadius, leveldirection, out hit, controller.enemyStats.lookRange, ~(1 << 9), QueryTriggerInteraction.UseGlobal)
                && hit.collider.CompareTag("Player"))
        {
            //In FOV and can see
            //Debug.DrawLine(controller.eyes.position, playertx.position, Color.red);

            //Update last known position
            controller.Last_Known_Position = hit.transform.position;

            controller.chaseTarget = hit.transform;
            return true;
        }
        else
        {
            //in FOV and can't see
            //Debug.DrawLine(controller.eyes.position, playertx.position, Color.yellow);
        }
    }
    else
    {
        //In FOV only
        //Debug.DrawLine(controller.eyes.position, playertx.position, Color.green);
    }

    return false;
}

Finally:
I have uploaded the latest code & build into Drop box.
https://www.dropbox.com/l/scl/AAC7WDc9lOSVaSVH42HuUFB5IifAKoI0on8

I guess you have to make an account to view and download them. If someone knows a better place to put them, let me know.

Ah controlsā€¦ I cant say what they are without saying WASD sorry. So WASD + Space & C to move the camera around. Left click to have UnityChan walk to a location.
I think G attacks, but really just triggers an animation at this time.

Alt-F4 to shut it down. There is no game controller so to speak, so the enemies will just run around randomly, chase you and start throwing haymakers when close.

4 Likes

Linux build up on DropBox!

4 Likes

Minor update:
Added spotlight onto the enemies, that shine with the state colour.
When enemies get to their destination, be it the players current or last known position, I added a rotation to face the destination.
There was a tendency for them to run directly to the player, bump them out of their position and face off a bit, leading to a stalled state sort of.

This leads to:

controller.transform.rotation = Quaternion.Lerp(controller.transform.rotation,
                                Quaternion.LookRotation(controller.chaseTarget.position - controller.transform.position, Vector3.zero),
                                Time.deltaTime * controller.enemyStats.LerpSpeed);

The Lerp function gradually transitions from one quaternionto another over successive frames. A quaternion is just a matrix that represents a rotation.

Bit more code tidy up.
I also slowed the enemies down a little, so you have a chance to outrun them in the maze.

4 Likes