Time Action Events (TAE)

Hello everyone
i wanna learn more about Animation events and create something close to the dark souls system for VFX, SFX and bullets
i used to mod dark souls 3 and create new monsters for the engine and learned a little about the system from-software uses

Actor’s models, textures and animations are save in compress bins called DCX
image
for animations in specific, you have 2 folders:
image

HKX are the animations themselves in the havok engine form
image

TAE is a text file than contains what to do on each frame

using dsanimationstudio we can put them together like this

to the right the animation and the left a representation of the TAE

you can see than things like “playing a sound from a dummy poly”, spawning a hitbox and spawning a visual effect for each track is save and use like this

i have found some very interesting post like this one than give me an initial idea how to start
(even tho i don’t understand the Sequence( part of the code )

some of the topics than i have no clue how to even start are:
+calling functions in animations
+Dummypolyes in panda
+calling functions from a text file (TAE file)
+using sfx in panda
+Hit and Hurt collision boxes

i think the best thing to start will be the Dummy polyes

they are simple objects attach to the model (like empties or bones) with only two variables

  • number of poly and
  • “Up” direction
    you can see the several with the same number so visual effects can be spawn for all the body
    and sound can be emitted from them
    image
    is there anyway to do this in panda with the egg models?

A “Sequence” is essentially an object that runs one or more intervals (or other Sequences, or Parallels) one after another. See also the “Parallel” class which runs one or more intervals (etc.) all at the same time.

So, in the example given in the thread to which you linked, the Sequence, once started, should first run an “ActorInterval” that goes through the first twelve frames of the given animation, then a type of Interval that calls a function–in this case the “footDown” function, specifically–and then finally another “ActorInterval” that goes through the final set of frames of the given animation.

In short, the Sequence given there runs part of an animation, calls a function, then runs the rest of the animation.

I fear that I’m still not clear on what, precisely, they are. You say that they have a “number of poly”–so does that mean that they have actual geometry attached to them?

You mention empties separately–which was my original guess as to what a “dummy poly” might be. I presume then that this isn’t the case?

So, could you perhaps explain further what a “dummy poly” is, please?

Hmm… This can be done, but to do it the simple way (that I see) invites a potential security hole: it would, essentially, allow someone with access to such files to run arbitrary Python code through your game–arbitrary Python code which might be malicious (or simply dangerous).

Otherwise, I daresay that one could implement such a thing: You could perhaps write a system that reads in a text-file, parses it, and then makes use of functions that you’ve already implemented as instructed in the file.

Have you looked through the below-linked section of the manual yet?
https://docs.panda3d.org/1.10/python/programming/audio/index

I fear that I’m still not clear on what, precisely, they are. You say that they have a “number of poly”–so does that mean that they have actual geometry attached to them?

sorry for been a little confusing, is more like a number ID

You mention empties separately–which was my original guess as to what a “dummy poly” might be. I presume then that this isn’t the case?

yes they are 100% empties only specific for the Dark souls engine (different name)

Aaah, I see now, I do believe! Thank you for the clarification. :slight_smile:

Well, there’s nothing to prevent you from having empties in Panda3D–whether loaded as part of an model or generated in code, they should be nothing more than empty PandaNodes, I believe.

As to attaching a number (or other data) to an empty PandaNode (or any node in Panda’s scene-graph, I believe), you can perhaps do that via either the “PythonTag” or “Tag” interface. The former allows for the attachment of general data (even instances of Python classes), while the latter only allows for the attachment of strings, but can be found by Panda’s scene-graph searching methods.

so for what i have seen empties are represented as a Group in the egg file
https://docs.panda3d.org/1.10/python/tools/model-export/egg-syntax?highlight=group#grouping-entries
so what can i use to attach objects or particles to it?

i presume exposeJoint doesn’t work in this case
here says than it returns the nodepath of the joint but in my case idk how the emply will be loaded

I think that “Group” entries are somewhat more general than that; I don’t think that studying them is terribly relevant to what you’re trying to do.

Now, an empty that was added in your modelling package is indeed not a joint–not part of the armature of the model–and thus presumably won’t respond to “exposeJoint” (or “controlJoint”).

However, if your model has a joint in its armature (a “bone” as it might be called), then you can simply use “exposeJoint” to gain access to it–this will give you a NodePath that is, essentially, an internally-generated empty that is controlled by the joint, and to which you can attach things as with any other node.

(If you do want to attach something to an empty that was added in your modelling package–i.e. not to a joint–then you should be able to search for it in the model (e.g. via the “find” method) and reparent the desired object to it as per usual.

However, if you want to attach something to a joint, then this isn’t terribly relevant to what you’re doing right now–I include it as something that might be useful for you to know.)

yeah you are right
i’m reconsidering my approach
it seems than finding the empties in an actor node maybe more difficult and inefficient than using the good old method of exposeJoint()
so i will create them from a text file relative to the joints and i guess i can add with them immediately the tag and maybe later an offset

dumi.txt
[dummies]
0 = Head 0
config.read('model/dumi.txt')
Dum = config.items('dummies')
sum = len(Dum)
j=0
for i in range(sum):
    data = Dum[j][1].split()
    self.i=self.t.exposeJoint(None, 'modelRoot', data[0])
    self.i.setTag("ID",data[1])
    j+=1
    def loadParticleConfig(self, filename):
        self.p.cleanup()
        self.p = ParticleEffect()
        self.p.loadConfig(Filename(filename))
        self.p.start(self.t)
        dumi = self.t.find("**/=ID=0")
        self.p.reparentTo(dumi)

the next question maybe how to spawn the same particle system in all the tagged “empties”
as reparenting one will just move it from one to another
can i instance and reparent a particle system?

I mean, I don’t know how the efficiency compares, but it has its uses–I’ve used empties to place things like NPC-spawners in some of my own projects, for example.

More to the point, however, it simply doesn’t do what you’re attempting to achieve. (That I know of, at any rate.)

By the way, looking at the following code:

It may be worth mentioning that Python offers a neat way of reducing what you have there: you don’t need an index in order to iterate over a list (or tuple, etc.) of objects–you can simply iterate over the objects themselves. Like so:

Dum = config.items('dummies')
for dummy in Dum:
    data = dummy[1].split()

I think that the reason that you’re encountering a problem there is that you’re storing the loaded particle effect in an instance-variable of your class, and then on each iteration cleaning up and replacing the contents of that instance-variable.

In short, a simple approach that I might suggest is to just create your particle effects in a local variable within the loop (i.e. no “self.”), attach them to your dummy-node, and then store them in a Python list or dictionary so that you can start them, stop them, and clean them up as you desire.

ok
it took me a while, but i have something close to what you said using global variables.
First i created two list, one to keep track of all the “dummies” and another for all the vfx’s
also i define the max number of vfx’s possible (max_vfx)
lvfx is a global variable to know the index of the last vfx spawned

list_dum = []
max_vfx = 10
lvfx = 0
list_vfx = []

then i load the model, the “dummies” and set the inputs from the keyboard

        base.enableParticles()
        config = configparser.RawConfigParser()

 # search for specific file with dummi positions positions

        config.read('model/Dummies.txt')
        Dum = config.items('Dummie')
        self.t = Actor("model/fox_tutorial",  # Load our animated charachter
                       {'walk': "model/fox_tutorial-walk_cycle"})
        self.t.reparentTo(render)  

# Put it in the scene

        self.t.actorInterval("walk", playRate=2).loop()

#if input 7 start function loadmultiVFX with arguments [Actor, particle file, Tag]

        self.accept('7', loadmultiVFX, [self.t, 'fireish.ptf', '1'])

#if input 8  disable all the tagged vfx's

        self.accept('8', disableallvfx)
        for dummy in Dum:
            data = dummy[1].split()
            list_dum.append(self.t.exposeJoint(None, 'modelRoot', data[0]))
            list_dum[-1].setTag("ID", data[1])

and then i have a vfx’s function than manage were and what to spawn from the inputs
and one to disable all in the list

def loadmultiVFX(t, filename, tag):
# t = Actor


#get variables lvfx and max_vfx

    global lvfx, max_vfx

# find tagged with (ID = tag ) in list of dummies  

    for i in list_dum:
        if i.getTag('ID') != tag:
            pass
        else:
            if lvfx >= max_vfx:
               #restart last vfx index if is bigger than max_vfx
                lvfx = 0
            if max_vfx != len(list_vfx):
                list_vfx.append(ParticleEffect())
                list_vfx[-1].loadConfig(Filename(filename))
                list_vfx[-1].start(t)
                list_vfx[-1].reparentTo(i)
                lvfx += 1
                print(f'last vfx {lvfx}')
            else:
                list_vfx[lvfx].disable()
                list_vfx[lvfx].loadConfig(Filename(filename))
                list_vfx[lvfx].start(t)
                list_vfx[lvfx].reparentTo(i)
                lvfx += 1
def disableallvfx():
    global lvfx
    lvfx  = 0
    for i in list_vfx:
        i.disable()

TAE fox spawn in legs and clean - YouTube

i still have some junk with the cycles and it only works if i disable() the particles, cleanup() brakes it, but that will be for future me.
the next step would be about hurt and hit boxes
i saw you did a fighting game
how did you manage them?

That said, I’m glad that you’ve made progress, it would seem! :slight_smile:

I am curious: Why place a limit on the number of particle effects? Why not just remove particle effects that you’re done with?

To clarify, I didn’t suggest that you use global variables, specifically, that I recall–a list that is an instance-variable should work, and produce less clutter to potentially trip you up later, I suspect.

I think that this is because you’re re-using your particle effects–I suspect that “cleanup” is intended as a means to destroy a particle-effect object, not a means to make it ready to load a new particle-file.

It’s been a while, but I daresay that my “world” class kept a list of particles (I was using my own particle-effect system, rather than Panda’s). New particles would I think have been added when created, and cleaned up and removed from the list when done.

I don’t think that I have the code for that game publicly available, but you might find my approach to something analogous to what you’re doing in an example-game that I made for one of my publicly-available modules. It’s perhaps a but much to go through, but search for the word “explosions” (note the “s” at the end), and you should find the relevant code.

Why place a limit on the number of particle effects? Why not just remove particle effects that you’re done with?

is just to have control on how many particle effects for all the entities i have on the scene (later) and idk how to make the particles autoclean after themselves, nothing more.
does particle factory life-time makes them auto disable?

I didn’t suggest that you use global variables

yes i know, the program had some trouble deciding from were to take the last vfx index because of the hole class is using self, so i had to do it like that

i can see you have the list like self and way more clear code

self.explosions = []
    def update(self, task):
        dt = globalClock.getDt()

        # Make the level-geometry fade its colour back and forth.
        perc = math.sin(globalClock.getRealTime()*0.5)*0.5 + 0.5
        self.levelGeometry.setColorScale(self.levelColour1*perc + self.levelColour2*(1.0 - perc))

        # Update our explosions, and remove any that have finished
        [explosion.update(dt) for explosion in self.explosions]
        deadExplosions = [explosion for explosion in self.explosions if explosion.timer <= 0]
        self.explosions = [explosion for explosion in self.explosions if not explosion in deadExplosions]

        for explosion in deadExplosions:
            explosion.root.removeNode()

the problem i found when cleanup() mine was than my list changed length mid cycle,
so i may change to something closer to your method in the future

but my question was more about how you made the hit-boxes or dealing damage

in dark souls they attach collision meshes (usually collision capsules or spheres) to the player’s weapon from one “dummy poly” in the weapon to another
like this
image
the capsule is for damage and the blue one is for collisions with walls

can i do it from the sequence or it may be too much?

and the “hurt”-boxes are just the same but connected to the skeleton
image

That’s somewhat fair–although do beware of premature optimisation!

Honestly, I’m not sufficiently familiar with Panda’s particle system to have the answer offhand, I’m afraid.

I’d be inclined to handle the cleanup in code, myself.

Ah, I see–and I’m glad if the code to which I linked you helped there!

Thank you. :slight_smile:

Aah, I see!

That should be quite possible–just a matter of reparenting your collision-nodes to your exposed joints! (… Which sounds rather grisly, put that way! XD; )

That way things that move the joints–including animations in sequences–should also move the hit-boxes.

One thing to consider is that fast movements with small hit-boxes against small enemies might result in those hit-boxes “skipping over” the enemies. However, the example-image that you gave shows a massive hit-box, and I seem to recall that Dark Souls has (somewhat) slow animations, so if you follow that pattern you may be fine. If not, then there are other, more-advanced approaches-but we’ll leave that until it actually proves called for, should it do so at all, I think!

(Again, I’m not sure that I remember offhand how I handled these matters in the game that you mentioned above.)

well my fox is moving when i command it

but no fire or water particle onsight
i think my particle functions are not been called somehow?
even when i created a function without arguments to call the particles… load_param()

list_anim = []
currTae = []
param =[]
import configparser
from direct.interval.IntervalGlobal import *


def TAE(t, wp, TAE):
    global list_anim
    list_anim.clear()
    config = configparser.RawConfigParser()
    config.read('TAE/' + str(TAE) + '.txt')
    Weapon_description = config.items('Info')
    object_data = config.items(str(wp))
    print(object_data)
    print(len(object_data))
    #cycle for anim
    for i in object_data:
        data = i[1].split("\n")
        anim = data[0].split(",")
        ani_name = anim[0]
        # get anim duration
        dur = (t.getNumFrames(ani_name))
        print(dur)
        # get framerate
        frameR = (t.getFrameRate(ani_name))

        # frames from anim

        k = dur / frameR

        ##create principal parallel
        p = Parallel(t.actorInterval(ani_name, startFrame=1, endFrame=dur))
        if anim[1] == "-1":
            break

        #cycle for tracks

        for b in range(1, int(anim[1])+1):
            # create track

            track = Sequence()

            print(f"track n = {track}")
            # cycle for searching functions
            for j in data[b].split(" || "):
                if j.split(",")[0] == "W":
                    d_t=(float(j.split(",")[2]) - float(j.split(",")[1])) * k
                    track.append(Wait(d_t))

                elif j.split(",")[0] == "A"
                    param.append(str(j.split(",")[1]) + "," + str(j.split(",")[2]))
                    I = len(param)-1
                    myfunc = Func(load_param)
                    track.append(myfunc)
                # if j.split(",")[0] == "s":
                #     track.append(funcsound())
                #     track.append(Wait(j.split(",")[3]))

                elif j.split(",")[0] == "C":
                    clean = Func(cleanallvfx)
                    track.append(clean)
        list_anim.append(p)

def load_param():
    params = param[currTae].split(",")
    print(params)
    TAEDemo.loadpartfromparam(params[0], params[1])

def TAEstart(n):
    global currTae
    currTae = n
    list_anim[n].start()

from this example file.txt

[Info]
Name = spear
[SPEAR]
;functions A=loadvfx(file,dummie) B=playsound(file,dummie)
; C = cleanallvfx() W= wait(fromframe1,toframe2)
0 = walk,2
W,0,10 || A,fireish.ptf,1 || W,12,15 || A,smoke.ptf,1 || C
W,0,12 || W,13,14

i get this

Parallel-1:
0.000 Parallel-1 {
0.000 *Actor-walk-1 dur 1
0.000 Sequence-2 {
0.000 Wait dur 10
10.000 *Func-load_param-1
10.000 Wait dur 3
13.000 *Func-load_param-2
13.000 *Func-cleanallvfx-3
13.000 }
0.000 Sequence-3 {
0.000 Wait dur 12
12.000 Wait dur 1
13.000 }
13.000 }

maybe is connected to how functions are called (or activated in a sequence?)
calling the function from any other way works

I don’t think so–they should work as expected, I believe. I daresay that it’s more likely an issue with the program logic implemented here.

Looking at your output, I don’t see anything that looks like the results of the print-statements in the “TAE” method–are you sure that that method is being called…?

By the way, I’m not convinced that your use of a global “currTae” variable to control which particle-system is to be loaded is likely to work: unless you have something in the sequence setting that value, it seems to me that you’ll likely end up with whatever value was last assigned to it.