How to make other actor model exactly follow the path that player just walk by

I have an idea of game that it will have a pet system.
I want my pet follow the path that player just walk by.

but I can’t imagine how to do it.
can I have some advice?

now I have these sort of codes. but it doesn’t do as I want yet.
(please feel free to let me know if this way doesn’t work)

import random
from math import ceil
from common import Common
from interface import DamageFrame
from piece import Seedbed
from item import *
from panda3d.core import CollisionBox, CollisionCapsule, Vec2, Vec3, Point2, Point3, TextNode
from direct.actor.Actor import Actor
from direct.gui.DirectFrame import DirectFrame
from direct.interval.IntervalGlobal import Sequence, Func, Wait
from direct.showbase.PythonUtil import fitDestAngle2Src

class Character():
    # Default atrributes
    model = "assets/models/King"
    center = Point3(-0.3, -0.3, 0)
    shape = Point3(0.3, 0.3, 1.25)
    collSurface = Point3(0.2, 0.2, 0)
    yVector = Vec2(0, 1)
    pos2d = Point2()
    update = True
    speed = 1
    run_speed = 2
    attack_speed = 1
    attackAnimDelay = 0.001
    dodge_force = 10
    dodge_distance = 1
    dodge_position = Point3(0, 0, 0)
    dodge_direction = Point3(0, 0, 0)
    dodge_key = False
    jump_force = 0.25
    jump_count = 0
    jump_limit = 2
    toAngle = 180
    is_attacking = False
    is_moving = False
    is_jumping = False
    is_blocking = False
    is_dodging = False
    is_on_floor = False
    is_dead = False
    is_changemap = True
    into_mask = 4
    from_mask = 1
    action_into_mask = False
    action_from_mask = 3
    response_into_mask = 3
    response_from_mask = False
    relation = 0
    regen = False

    def __init__(self, pos=False):

        # Default position/velocity/acceleration
        self.name = self.__class__.__name__.replace("_", " ")
        self.position = Vec3(0, 0, 0)
        self.velocity = Vec3(0, 0, 0)
        self.acceleration = Vec3(0, 0, 0)

        # new generate object key
        self.objKey = int(max([*Common.ShowBase.objectList], default=0))+1 if Common.ShowBase.objectList else 0

        # Create actor model
        self.obj = Actor(self.model)
        self.obj.setScale(0.5)
        self.obj.reparentTo(Common.ShowBase.render)

        if pos is not False:
            pos = Common.ShowBase.getSqrPos(pos)
            self.position = pos

        coll_box = CollisionBox(self.center, self.shape)
        collider = Common.ShowBase.setCollisionHandler(
            collSolid=coll_box, 
            IntoMask=self.into_mask, 
            FromMask=self.from_mask, 
            event=True,
            queue=True,
            collName='char-node'
        )
        collider.reparentTo(self.obj)
        collider.node().setTag("char", str(self.objKey))

        # Response Collider
        coll_box = CollisionBox(self.center, self.shape)
        self.action_collider = Common.ShowBase.setCollisionHandler(
            collSolid=coll_box, 
            IntoMask=self.response_into_mask,
            FromMask=self.response_from_mask,
            queue=True,
            collName='char-node'
        )
        self.action_collider.reparentTo(self.obj)
        self.action_collider.node().setTag("char", str(self.objKey))

        # Action Collider
        coll_box = CollisionCapsule((0, 0.6, 0.5), (0, 1.25, 0.5), 0.25)
        self.action_collider = Common.ShowBase.setCollisionHandler(
            collSolid=coll_box, 
            IntoMask=self.action_into_mask,
            FromMask=self.action_from_mask,
            queue=True,
            show=True,
            collName='action-node'
        )
        self.action_collider.reparentTo(self.obj)
        self.action_collider.node().setTag("char", str(self.objKey))

        # Create an HUD
        self.HUD = DirectFrame(
            frameColor=(0, 0, 0, 0), 
            frameSize=(-0.15, 0.15, -0.25, 0.25), 
            parent=Common.Interface.gameGUI
        )
        self.HUD.setBin("HUD", 0)

        text = TextNode('name')
        text.setFont(Common.Interface.font)
        text.setText(self.name)
        text.setTextScale(0.06)
        text.setTextColor(1, 1, 1, 1)
        text.setAlign(TextNode.ACenter)
        textNode = Common.ShowBase.render2d.attachNewNode(text)
        textNode.setPos((0, 0, 0.2))
        textNode.reparentTo(self.HUD)
        textNode.hide()

        # Append object to base
        Common.ShowBase.charList[self.objKey] = { "from": self, "into": None }
        Common.ShowBase.objectList[self.objKey] = self
        setattr(Common.ShowBase, self.__class__.__name__.lower(), self)

        self.atkseq = Sequence(Wait(self.attackAnimDelay), Func(self.startOffense), Wait(self.attack_speed), Func(self.finishOffense))
        self.defseq = Sequence(Func(self.startDefense), Wait(1))
        self.dgeseq = Sequence(Func(self.startDodge), Wait(0.5), Func(self.finishDodge))
        self.regen = Sequence(Wait(5), Func(self.updateRegeneration))
        self.regen.loop()

class Ally(Character):
    type = "pet"
    targetType = ("monster")
    attackAnimDelay = 1
    attackDistance = 0.75
    traceDistance = 8.5
    is_tracing = False
    speed = 0.3
    mobility = True
    aggressive = False

    def __init__(self, pos=False):
        super().__init__(pos)

        Common.ShowBase.taskMgr.add(self.updateTracking, "updateTracking")

    def updateTracking(self, task):
        # try:
        dt = globalClock.getDt()
        fromAngle = self.obj.getH()

        target = self.findTarget()

        vectorToTarget = target['coordinates']
        vectorToTarget2D = target['heading']
        distanceToTarget = target['distance']
        trackToAttack = target['trackToAttack']

        if trackToAttack is not False:
            if self.aggressive is not False and distanceToTarget < self.attackDistance * self.traceDistance:
                self.is_tracing = True

            if self.is_tracing is not False and distanceToTarget < self.attackDistance * self.traceDistance:
                if not self.is_attacking:
                    heading = self.yVector.signedAngleDeg(vectorToTarget2D)
                    self.obj.hprInterval(0.05, (fitDestAngle2Src(fromAngle, heading), 0, 0)).start()
            else:
                self.is_tracing = False

            if self.mobility is not False and self.is_tracing is not False:
                if not self.is_attacking and distanceToTarget > self.attackDistance and distanceToTarget < self.attackDistance * self.traceDistance:
                    vectorToTarget.normalize()
                    self.velocity += vectorToTarget * self.speed * dt
                else:
                    self.velocity.set(0, 0, 0)

            if not self.is_attacking and distanceToTarget <= self.attackDistance:
                self.doOffense()
        else:
            if not self.is_attacking:
                heading = self.yVector.signedAngleDeg(vectorToTarget2D)
                self.obj.hprInterval(0.05, (fitDestAngle2Src(fromAngle, heading), 0, 0)).start()

            if distanceToTarget > 0.75:
                vectorToTarget.normalize()
                self.velocity += vectorToTarget * Common.ShowBase.player.speed * dt
            else:
                self.velocity.set(0, 0, 0)
        # except:
        #     print("An exception occurred on \"updateTracking\"")
        return task.cont

    def findTarget(self):
        target = False
        enemyList = dict()
        for i in Common.ShowBase.charList:
            if Common.ShowBase.charList[i]['from'].type in self.targetType:
                target = Common.ShowBase.charList[i]['from']
                vectortoTarget = target.obj.getPos() - self.obj.getPos()
                vectortoTarget2D = vectortoTarget.getXy()
                distanceToTarget = vectortoTarget2D.length()
                enemyList.update({i: {"coordinates": vectortoTarget, "heading": vectortoTarget2D, "distance": distanceToTarget, "trackToAttack": True}})
        if enemyList:
            target = min(enemyList.items(), key=lambda x: x[1]['distance'])[1]

        if target is not False and target['distanceToTarget'] < self.attackDistance * self.traceDistance:
            return target
        else:
            vectortoTarget = Common.ShowBase.player.obj.getPos() - self.obj.getPos()
            vectortoTarget2D = vectortoTarget.getXy()
            distanceToTarget = vectortoTarget2D.length()
            return {"coordinates": vectortoTarget, "heading": vectortoTarget2D, "distance": distanceToTarget, "trackToAttack": False}

class Robot(Ally):

    def __init__(self, pos=False):
        Ally.__init__(self, pos)

        self.status = { "hp":100, "sp":10, "mp":0, "maxhp":100, "maxsp":10, "maxmp":0, "patk":1, "pdef":1, "matk":1, "mdef":1, "gold":0 }

Without having really looked through the code, I’d suggest something along the following lines:

  • As the player walks, record at intervals their position; store these positions in a list
  • Use the resulting list as a set of waypoints for the follower, specifically:
    • While walking, the following moves towards the first (and thus oldest) position in the list
    • When that position is reached, it is removed from the list
      • This then results in the second-oldest position, which should be the next in line, becoming the first in the list and thus the one that the follower moves towards.
      • I would suggest that the test for the position being reached have a degree of tolerance–that is, that it test not for the ally being in exactly the same place (which may be finicky, if it happens at all), but rather for the ally being near to the desired place.

Note that this is a fairly simplistic approach, and may call for some nuances beyond the basis outlined above. (For example, you might want to use some vector-tests to check that the follower hasn’t moved past the current target-point.)

Still, I think that it should overall work.

1 Like

panda3d has AI steering behaviors support, you will need the “seek” or “chase” and collision avoidance behavior.

This is industry standard approach, since you will want your AI character to properly navigate around the environment without colliding with scene geometry or other AI characters, the steering behaviors properly handle this aspect.

1 Like

Thank you both for your advice.
Now I have an idea to deal with this issue.

1 Like

Update

now I already solve this issue.
(There is some problem needs to be fixed but still in the way which I expected)

I use movement loop task to append point that Player’s model was pass into wayPoint list.

Then I have pet model traced a first element of wayPoint list.
After pet model approach it then pop first element wayPoint list off .
then let pet model find next first element of wayPoint list.

I found that PandaAI is not suit to this issue yet.
but maybe it will suit to some method which I have planned in future.

(all models now are assume as its role. I still need to learn how to use Blender)

1 Like