• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

Some OO stuff without structs

Status
Not open for further replies.
Level 21
Joined
Mar 27, 2012
Messages
3,232
(Design stuff. Might not be interesting for most.)

So recently I've been redesigning a lot of my code.
When making my buff system I realized something fairly important - many effects that I wrote for using with buffs would actually make tons of sense as more generic code. For instance, although a buff can put special effects on your unit, so can items, passive abilities and maybe something else.
My code didn't account for that possibility at all, so I continued to think about what it is exactly that I want.
Eventually I realized that what I really want is a way to treat every single thing as an interchangeable object. That is, to use object-oriented programming to a degree that I've never seen done with structs. Heck, it might even be pointlessly complicated when using structs for it.
I created this piece of code to act as the core of everything:
JASS:
library Object requires Utils
    globals
        public constant integer MaxIndices =  8191//Default is 8191. Any number above it carries a (small) performance penalty and adds some lines of code.
        
        public integer array Type[MaxIndices]
        public integer array Parent[MaxIndices]
        public integer array FirstChild[MaxIndices]
        public integer array ChildCount[MaxIndices]
        public integer array Next[MaxIndices]
        public integer array Prev[MaxIndices]
        public string array TypeName
        public trigger array CreateMethod
        public trigger array DestroyMethod
        public trigger array AddMethod
        public trigger array RemoveMethod
        public trigger array RefreshMethod
        public integer Types = 0
        public integer Allocs = 0
        public integer array Recycle[MaxIndices]
        public integer Recycles = 0
        public integer Object
    endglobals
    public function RegisterType takes string name returns integer
        set Types = Types + 1
        set TypeName[Types] = name
        return Types
    endfunction
    public function RegisterTypeMethods takes integer otype, code create,code destroy,code add,code remove, code refresh returns nothing
        set CreateMethod[otype] = CreateTrigger()
        call TriggerAddCondition(CreateMethod[otype],Condition(create))
        set DestroyMethod[otype] = CreateTrigger()
        call TriggerAddCondition(DestroyMethod[otype],Condition(destroy))
        set AddMethod[otype] = CreateTrigger()
        call TriggerAddCondition(AddMethod[otype],Condition(add))
        set RemoveMethod[otype] = CreateTrigger()
        call TriggerAddCondition(RemoveMethod[otype],Condition(remove))
        set RefreshMethod[otype] = CreateTrigger()
        call TriggerAddCondition(RefreshMethod[otype],Condition(refresh))
    endfunction
    public function CreateId takes nothing returns integer
        local integer id
        if Recycles == 0 then
            set Allocs = Allocs + 1
            set id = Allocs
        else
            set id = Recycle[Recycles]
            set Recycles = Recycles - 1
        endif
        return id
    endfunction
    public function Create takes integer otype returns integer
        local integer obj = CreateId()
        debug if otype > Types or otype < 1 then
            debug call Print("CreateObject: Invalid otype"+"("+I2S(otype)+")")
        debug endif
        set Type[obj] = otype
        set Object = obj
        call TriggerEvaluate(CreateMethod[otype])
        return obj
    endfunction
    public function Add takes integer obj,integer parent returns nothing
        local integer next
        local integer prev
        if ChildCount[parent] == 0 then
            set FirstChild[parent] = obj
            set Next[obj] = obj
            set Prev[obj] = obj
        else
            set next = FirstChild[parent]
            set prev = Prev[next]
            set Next[prev] = obj
            set Prev[next] = obj
        endif
        set ChildCount[parent] = ChildCount[parent] + 1
        set Parent[obj] = parent
        set Object = obj
        call TriggerEvaluate(AddMethod[Type[obj]])
    endfunction
    public function Remove takes integer obj returns nothing
        local integer parent = Parent[obj]
        local integer next
        local integer prev
        set Object = obj
        call TriggerEvaluate(RemoveMethod[Type[obj]])
        if ChildCount[parent] > 1 then
            if obj == FirstChild[parent] then
                set FirstChild[parent] = next
            endif
            set next  = Next[obj]
            set prev = Prev[obj]
            set Next[prev] = next
            set Prev[next] = prev
            set ChildCount[parent] = ChildCount[parent] - 1
        else
            set FirstChild[parent] = 0
            set ChildCount[parent] = 0
        endif
        set Next[obj] = 0
        set Prev[obj] = 0
    endfunction
    public function Destroy takes integer obj returns nothing
        if Parent[obj] != 0 then
            call Remove(obj)
        endif
        loop
            exitwhen FirstChild[obj] == 0
            call Destroy(FirstChild[obj])
        endloop
        set Object = obj
        call TriggerEvaluate(DestroyMethod[Type[obj]])
        set Type[obj] = 0
        set Recycles = Recycles + 1
        set Recycle[Recycles] = obj
    endfunction
endlibrary
As anyone reading the code might see, this library does very little on its own. All it does is allow creation of generic object types, which can be organized in a tree-like structure. Note that all types are created equal, meaning they share the same arrays and as such, the amount of objects can quickly get out of hand. In my stress tests of spells I have reached around 5k objects by spamming just 1 spell.
It lagged a lot, of course, but I imagine there are ways to reach the limit without killing the game, so although I hope not to use it, I made sure it would be possible to extend the number of indices through changing a single variable.

A considerable pitfall with my design - The system so far does not allow subtypes, because I couldn't figure out a way to do typechecks reasonably in a way that actually makes things easier.
As such, I can easily make the type "buff", but I won't have any explicit way of making specific types of buffs.
It's not a problem though, as I can just add a new type per bufftype, which would also provide me a handy way to create buffs as autocreating structures.
That is, I can make a bufftype (a separate type per bufftype) object add other objects to its parents - namely the effects of the buff.

One might ask why I would make a system for this instead of just linking things in code. The answer is that I can't know every possible interaction. It could very well be that a hero(1.) has an item(2.), which has an ability(3.) that spawns a minion(4.) that has its own passive(5.), which burns units around it by applying a buff on them(6.).
I could want to make it so that this item is some kind of a king-of-the-hill type relic, in which case it would make tons of sense to cancel its effects upon dropping it(through death). However, this means that I would have to hardcode that when this happens, the item checks for its own minions and removes them while paying respect to every single aspect of their existence(the immolate ability(6.)).
If I hardcoded things like this it would certainly be somewhat easier initially, but the code itself would be a mess and I wouldn't be able to maintain it at all. Every little change would have the potential to create tons of problems.

Yes, I was bored. But I also want to know what other people think of this kind of a unified approach. It has simplified my coding quite a lot, while still letting me keep absolute control over everything.
 
Level 13
Joined
Nov 7, 2014
Messages
571
One might ask why I would make a system for this instead of just linking things in code. The answer is that I can't know every possible interaction. It could very well be that a hero(1.) has an item(2.), which has an ability(3.) that spawns a minion(4.) that has its own passive(5.), which burns units around it by applying a buff on them(6.).
I could want to make it so that this item is some kind of a king-of-the-hill type relic, in which case it would make tons of sense to cancel its effects upon dropping it(through death). However, this means that I would have to hardcode that when this happens, the item checks for its own minions and removes them while paying respect to every single aspect of their existence(the immolate ability(6.)).
If I hardcoded things like this it would certainly be somewhat easier initially, but the code itself would be a mess and I wouldn't be able to maintain it at all. Every little change would have the potential to create tons of problems.

Maybe you should write the "king-of-the-hill type relic" in both vJass with "hardcoding" style
and in the "Object+Utils" style. Then we could compare the two implementations and try to arrive at
a conclusion of the usefulness of the "structless approach to OO".

Otherwise it would be pretty hard to compare the two without a usage code.

From what I can see though, it seems that you would lose the nifty struct_instance.member syntax
and pay for the array lookups turning into function calls after using more than 8191 Objects.

Also, where is the storage for the different otype's members located in the "Object+Utils" style?
 
Level 21
Joined
Mar 27, 2012
Messages
3,232
Maybe you should write the "king-of-the-hill type relic" in both vJass with "hardcoding" style
and in the "Object+Utils" style. Then we could compare the two implementations and try to arrive at
a conclusion of the usefulness of the "structless approach to OO".

Otherwise it would be pretty hard to compare the two without a usage code.

From what I can see though, it seems that you would lose the nifty struct_instance.member syntax
and pay for the array lookups turning into function calls after using more than 8191 Objects.

Also, where is the storage for the different otype's members located in the "Object+Utils" style?

I'd say one of the most important differences is maintainability. Although hardcoding is easier initially, it is prone to breaking when you change things.

Well in fact, the dot syntax doesn't really grant any extra functionality. It just changes the position of one parameter.

Depends on what storage you mean. Every object in itself is just an array index, so you can massively utilize parallel arrays just like you do with a unit indexer. Btw, I have written a unit indexer that complies with this methodology, as part of the "unit" type.
Maybe one of the coolest things with this design is that things can be cleaned up without any regard for what they are after they have been defined. Destroying one object automatically destroys its children, so when you have a summoner, its summons die automatically when it gets removed from the game. This means that false references are prevented in the first place. If such a functionality is undesired, it's possible to just not list summons in the main list of its parent. You can have some list of your own that isn't checked by the system.

I haven't thought much about the sized arrays approach yet. Currently I have it to provide an emergency option in case something goes wrong, but there is the possibility that I will have a toggle in the system to handle a lot of it through a hashtable instead. The reason is that although arrays are faster, having to call a function(sized arrays) can make the performance difference negligible. If so, I'd be better off just using hashtables.
 
Level 21
Joined
Mar 27, 2012
Messages
3,232
Well, here's a small map where I test various things. There's also some other stuff(chat commands, yay).

There aren't all so many examples of how to use Object yet though, although I imagine the most explicit thing would be a buff system.

EDIT: Attached another map. The buff system in BW2 is basically the same thing, but less generalized. It's the reason I decided to remake things in the first place, because I realized that many of the effects could be applied through other things than buffs too, logic-wise, but the system wasn't designed for that.
There's many ways to use such a system really. It can also be an everything-indexer in the sense that you can make a wrapper for every kind of object that exists and then be able to manage them easier and refer to them specifically.
 

Attachments

  • Systems.w3x
    41.7 KB · Views: 47
  • Brute Wars 2.11.w3x
    906.7 KB · Views: 50
Level 13
Joined
Nov 7, 2014
Messages
571
Maybe I don't understand it (because I've never written a buff system) but I would always
prefer the more "encapsulated"/declarative/structured (in my opinion) syntax of structs:

JASS:
struct Vec extends array
    real x
    real y
    real z

    private static thistype self = 0
    static method new takes nothing returns thistype
        // Simplest allocation scheme possible =)
        // [1, 2, ..., 8190, 1, 2, ..., 8190, etc.]
        local thistype this = self + 1
        if this > 8190 then
            set self = 1
            set this = 1
        else
            set self = this
        endif

        set x = 3
        set y = 5
        set z = 9

        return this
    endmethod

    static method from_xyz takes real x, real y, real z returns thistype
        local thistype this = thistype.new()
        set this.x = x
        set this.y = y
        set this.z = z
        return this
    endmethod

    method length takes nothing returns real
        return SquareRoot(x * x + y * y + z * z)
    endmethod
endstruct

struct UseVec extends array
    static method onInit takes nothing returns nothing
        local Vec vec = Vec.new()
        local real vec_length = vec.length()
        local real x = vec.x
    endmethod
endstruct

even if it doesn't give you "Destroying one object automatically destroys its children":

JASS:
library Vec initializer in requires Object

    globals
        integer OType_Vec
        real array Xs
        real array Ys
        real array Zs
    endglobals

    private function OnCreate takes nothing returns nothing
        set Xs[Object_Object] = 3
        set Ys[Object_Object] = 5
        set Zs[Object_Object] = 9
    endmethod

    public function Create takes nothing returns integer
        return Object_Create(OType_Vec)
    endfunction

    public function FromXyz takes real x, real y, real z returns integer
        local integer vec = Object_Create(OType_Vec)
        set Xs[vec] = x
        set Ys[vec] = y
        set Zs[vec] = z
        return vec
    endfunction

    public function Length takes integer vec returns real
        return SquareRoot(Xs[vec] * Xs[vec] + Ys[vec] * Ys[vec] + Zs[vec] + Zs[vec])
    endfunction

    private function in takes nothing returns nothing
        set OType_Vec = Object_RegisterType("Vec")
        call Object_RegisterTypeMethods(Otype_Vec, function OnCreate, /*Destroy*/null, /*Add*/null, /*Remove*/null, /*Refresh*/null)
    endfucntion

endlibrary

library UseVec initializer in requires Vec

    private function in takes nothing returns nothing
        local integer vec = Vec_Create()
        local real vec_length = Vec_Length(vec)
        local real x = Xs[vec]
    endfunction

endlibrary

Yeah it's a contrived example/comparison, but I really like vJass's structs;
they give me a "warm fuzzy feeling" (not really a "technical explanation" =)).

But then again if "It has simplified my coding quite a lot, while still letting me keep absolute control over everything.",
I guess you should use/explore it further.
 
Level 21
Joined
Mar 27, 2012
Messages
3,232
Maybe I don't understand it (because I've never written a buff system) but I would always
prefer the more "encapsulated"/declarative/structured (in my opinion) syntax of structs:

Yeah it's a contrived example/comparison, but I really like vJass's structs;
they give me a "warm fuzzy feeling" (not really a "technical explanation" =)).

But then again if "It has simplified my coding quite a lot, while still letting me keep absolute control over everything.",
I guess you should use/explore it further.

Well, it is shorter but I don't consider it worth it that structs obfuscate the code. You don't really know what it really looks like unless you look in the map script, so in addition to any bugs you might create on your own you also have to deal with the struct implementation.
I find that it's more reasonable to just have the option to change any part of the implementation if it doesn't suit your needs.
But then again, as you said, it is a matter of preference really.
Btw, you have one unnecessary function in the lower example. If you just made OnCreate public and removed Create it would be equivalent to the upper example.

So, why not just use a tree data structure?...

We also have this

https://github.com/nestharus/JASS/blob/master/jass/Systems/Type/script.j

So basically when creating a new type you copy all its parents to its own hashtable, so you can test it against specific types. Interesting approach.

Judging by my code, what difference would a tree make? Is it not a tree already then?
 
Status
Not open for further replies.
Top