You would define one UDT per "class" (using C++ terminology), then create instances of the "class", which would include the Program structure (.$pgm member) and instance data (UDT) structure (.$data member).
Say this is a packing machine, there would be 1 code-block to run 10 packing maches, say the code-block is called PackingMachine. Say each one uses a couple Timers, FillTimer and SealTimer. For simplicity, let's say you have 10 machines, MachineA thru MachineJ.
You would write ONE code block using PackingMachine. Say it was a Stage program, so you would write your code ONCE:
SG PackingMachine.$pgm.S0
...
SG PackingMachine.$pgm.S1
...
SG PackingMachine.$pgm.S2
...
inside Stage S2, say you use the FillTimer
PackingMachine.$pgm.S2
SG PackingMachine.$pgm.S2
TMR PackingMachine.$data.FillTimer
STR PackingMachine.$data.FillTimer.Done
JMP PackingMachine.$pgm.S3
But when the code EXECUTES for the 10 different INSTANCES in one PLC scan, it basically runs that code block 10 times, but instead of using PackingMachine, it uses 10 individual SPECIFIC INSTANCES (same code - not a copy of the code, but each execution instance at runtime just replaces PackingMachine with MachineA, then MachineB, then MachineC, ... MachineJ.
Each INSTANCE has its own program/data state, but just ONE code-block (ran 10 different times with 10 different object states). So then MachineA can be in Stage S0, MachineB in Stage S1, MachineC in Stage S2 with MachineC.$data.FillTimer.Acc equal to 1234, MachineD in Stage S2 with MachineD.$data.FillTimer.Acc equal to 5678.
If you want to see the status of the logic for MachineA, open up THAT INSTANCE, turn status ON. For MachineJ, open up THAT INSTANCE, turn status ON. THAT code will be SIMILAR to the ORIGINAL code-block (PackingMachine), but will instead show instruction status of instance MachineJ as follows (say for Stage S2):
SG
MachineJ.$pgm.S2
TMR
MachineJ.$data.FillTimer
STR
MachineJ.$data.FillTimer.Done
JMP
MachineJ.$pgm.S3
If you had a big enough screen, you could tile the 10 code INSTANCED blocks, all with status ON. The
shape of the 10 Ladder Views would be IDENTICAL, but the INDIVIDUAL OBJECT INSTANCE PARAMETERs would be SWAPPED OUT.
You create/edit using the base PackingMachine code block, but you do status with the INSTANCE based code blocks (MachineA thru MachineJ). You can't "edit" MachineJ's "copy", because MachineA thru MachineJ SHARE THE SAME BASE CODE (i.e. PackingMachine).
Since you would be limited to just 256 bytes in the $data portion of the "class", how many timers, counters, PID, nested UDTs will you need? A Timer is 8 BYTEs. A REAL is 4 BYTEs. A PID is 96 BYTEs. A string can be 8 BYTEs or 64 BYTEs or

. So you can burn through 256 BYTEs easily if you need a couple PIDs or a bunch of non-trivial strings. Hence, do we need 2 (or 3) UDTs to hold all the necessary "member" variables (Timers, PID, Strings, Recipe UDT, other UDT, etc. etc.) .$data1 .$data2 .$data3 ? ? ?