Archived

This topic is now archived and is closed to further replies.

Please be aware that the content of this thread may be outdated and no longer applicable.

MaiTerra

Asymmetcrical Character Design?

Recommended Posts

MaiTerra    1

So, I'm partway through modding a character for Don't Starve Together and I'm using the Extended Sample Character template which has been great and all, but once I started on the animation artwork for the character I realized a slight problem. The character's design is fairly asymmetrical (hair on one side of the head is styled differently than on the other side for one thing), and I've noticed that from the looks of it, Don't Starve and Don't Starve Together just flip the sideview images of a character depending on whether they're facing left or right, rather than using different images for different sides. So, I'm basically just wondering if there's a way to have the game use different images for the character depending on which side they're facing instead of just flipping them? (And of course if so, how would I do that?) Hopefully my question makes sense! I don't know if I wrote in out in a confusing way or anything.

Share this post


Link to post
Share on other sites
BrotatoTips    23

if anything, the most practical answer for now is to recreate all of the animations along with a perfectly custom character template that utilizes your asymmetrical design. well, this in turn is very complicated and tedious. also, you probably wouldnt want to do this and it isnt very practical. but this is one method you could take. another possibility is making two separate character templates and name them 1 and 2. and make a separate anim build for these and have it utilize these also. so when left, it would display the anim directed at 1, and when facing right, vice versa. but all this would be difficult, and may in turn, not be possible. i assume someone else has a better answer than I, but in the downtime, at least i gave you an interesting read. (this kinda stuff works with items and such, and it would take some coding, but its plausible).

Share this post


Link to post
Share on other sites
Ryuushu    43

@MaiTerra
 
After playing around for a while, it's quite possible, no need to remake all animations from scratch. And it's rather easy.
 
JpXMlB4.gif?1

 

Notice the eye color.

 

Anyways, here's how you do it:

First, save your current Spriter project as mycharacter_build_right. The side view will be the side that'll show when your character is facing right.

Copy-paste your project folder (Spriter project + images) to a new folder. Then in that new Spriter project modify the side view images to your liking. Don't worry about your character facing right. Save your spriter project as mycharacter_build_left.

Let the autocompiler do its job and throw your new anims in your mod anim folder. In your character file add the new anim assets.

Now comes the fun part; handling which anim build we should use. We want to use mycharacter_build_right when it's facing right, and mycharacter_build_left when it's facing left. So, when do we know the character has started facing a side? We could use the event "locomote". That event will be sent when the character moves. So, we just add a listener to that event and use a bit of trickery to figure out what direction is the character facing:

    inst:ListenForEvent("locomote", function(inst)        if inst:HasTag("playerghost") then return end                local dir = inst.Transform:GetRotation()        local camera_rot = TheCamera:GetHeadingTarget()                print("Camera rotation:", camera_rot)        print("Direction:", dir)                if camera_rot < 0 then            camera_rot = camera_rot + 360        end                if camera_rot == 0 or camera_rot == 45 then            if dir <= 0 then                inst.AnimState:SetBuild("esctemplate_right")            else                inst.AnimState:SetBuild("esctemplate_left")            end        elseif camera_rot == 90 or camera_rot == 135 then            if math.abs(dir) >= 90 then                inst.AnimState:SetBuild("esctemplate_right")            else                inst.AnimState:SetBuild("esctemplate_left")            end        elseif camera_rot == 180 or camera_rot == 225 then            if dir >= 0 then                inst.AnimState:SetBuild("esctemplate_right")            else                inst.AnimState:SetBuild("esctemplate_left")            end        else--if camera_rot == 270 or camera_rot == 315 then            if math.abs(dir) <= 90 then                inst.AnimState:SetBuild("esctemplate_right")            else                inst.AnimState:SetBuild("esctemplate_left")            end        end    end)end

Basically what we do here is change our images depending on what side the character is facing.

 

This should work relatively well. I haven't tested it with multiple players, but it should play along nicely. Also, there is another problem: The camera can keep rotating and go out of our scope. This should be solved by using the modulo operation ( % ) and then comparing. There might be some small problems with doing an action and then rotating the camera (like fishing), but nothing gamebreaking.

Share this post


Link to post
Share on other sites
MaiTerra    1

@Ryuushu Thank you! However, it seems to cause a crash whenever I enable the mod after doing this. I think I followed the instructions correctly, as far as I'm aware, though it's entirely possible I missed something since I'm still pretty new to modding and stuff. I'm assuming this code goes in the character's file in the mod's prefab folder, right? Is there anything I should make sure goes before or after this code or can it go more or less anywhere in the file?

Share this post


Link to post
Share on other sites
Ryuushu    43

@MaiTerra

It goes in the character's prefab file, inside master_postinit. Did you remember to load the anim assets? Hmm, posting the crash screen/error line helps too.

Share this post


Link to post
Share on other sites
MaiTerra    1

Oh! I got it working! The problem was that I wasn't putting it in master_postinit, silly me. But yes, it works now, thank you so much for the help!

Share this post


Link to post
Share on other sites
Ryuushu    43

@MaiTerra
I was poking around a bit more and figured out how to make idles work:
 


--Remember to replace esctemplate with your character's prefab namelocal function ChangeAnim(inst)    if inst.prefab ~= "esctemplate" or inst:HasTag("playerghost") then return end        local dir = inst.Transform:GetRotation()    local camera_rot = GLOBAL.TheCamera:GetHeadingTarget() % 360        print("Camera rotation:", camera_rot)    print("Direction:", dir)        if camera_rot == 0 or camera_rot == 45 then        if dir <= -5 then            inst.AnimState:SetBuild("esctemplate_right")        else            inst.AnimState:SetBuild("esctemplate_left")        end    elseif camera_rot == 90 or camera_rot == 135 then        if math.abs(dir) >= 90 then            inst.AnimState:SetBuild("esctemplate_right")        else            inst.AnimState:SetBuild("esctemplate_left")        end    elseif camera_rot == 180 or camera_rot == 225 then        if dir >= -5 then            inst.AnimState:SetBuild("esctemplate_right")        else            inst.AnimState:SetBuild("esctemplate_left")        end    else--if camera_rot == 270 or camera_rot == 315 then        if math.abs(dir) <= 90 then            inst.AnimState:SetBuild("esctemplate_right")        else            inst.AnimState:SetBuild("esctemplate_left")        end    endendlocal FRAMES = GLOBAL.FRAMESlocal TimeEvent = GLOBAL.TimeEventlocal function sg_postinit(sg)    local old_locomote_event = sg.events.locomote.fn    sg.events.locomote.fn = function(inst, data)        ChangeAnim(inst)        return old_locomote_event(inst, data)    end        if GLOBAL.TheWorld.ismastersim then        local old_fishingcancel_event = sg.events.fishingcancel.fn        sg.events.fishingcancel.fn = function(inst)            ChangeAnim(inst)            return old_fishingcancel_event(inst)        end    end        -- Add/Remove TimeEvents as you see fitting. More TimeEvents = more accurate animations. Less TimeEvents = Slightly better performance.    table.insert(sg.states["idle"].timeline, TimeEvent(20*FRAMES, function(inst) ChangeAnim(inst) end))    table.insert(sg.states["idle"].timeline, TimeEvent(30*FRAMES, function(inst) ChangeAnim(inst) end))    table.insert(sg.states["idle"].timeline, TimeEvent(40*FRAMES, function(inst) ChangeAnim(inst) end))    table.insert(sg.states["idle"].timeline, TimeEvent(50*FRAMES, function(inst) ChangeAnim(inst) end))    table.insert(sg.states["idle"].timeline, TimeEvent(60*FRAMES, function(inst) ChangeAnim(inst) end))    table.insert(sg.states["idle"].timeline, TimeEvent(70*FRAMES, function(inst) ChangeAnim(inst) end))    table.insert(sg.states["idle"].timeline, TimeEvent(80*FRAMES, function(inst) ChangeAnim(inst) end))    table.insert(sg.states["idle"].timeline, TimeEvent(90*FRAMES, function(inst) ChangeAnim(inst) end))    table.insert(sg.states["idle"].timeline, TimeEvent(100*FRAMES, function(inst) ChangeAnim(inst) end))        if GLOBAL.TheWorld.ismastersim then        table.insert(sg.states["funnyidle"].timeline, TimeEvent(20*FRAMES, function(inst) ChangeAnim(inst) end))        table.insert(sg.states["funnyidle"].timeline, TimeEvent(30*FRAMES, function(inst) ChangeAnim(inst) end))        table.insert(sg.states["funnyidle"].timeline, TimeEvent(40*FRAMES, function(inst) ChangeAnim(inst) end))        table.insert(sg.states["funnyidle"].timeline, TimeEvent(50*FRAMES, function(inst) ChangeAnim(inst) end))        table.insert(sg.states["funnyidle"].timeline, TimeEvent(60*FRAMES, function(inst) ChangeAnim(inst) end))        table.insert(sg.states["funnyidle"].timeline, TimeEvent(70*FRAMES, function(inst) ChangeAnim(inst) end))        table.insert(sg.states["funnyidle"].timeline, TimeEvent(80*FRAMES, function(inst) ChangeAnim(inst) end))    end        table.insert(sg.states["fishing"].timeline, TimeEvent(20*FRAMES, function(inst) ChangeAnim(inst) end))    table.insert(sg.states["fishing"].timeline, TimeEvent(40*FRAMES, function(inst) ChangeAnim(inst) end))    table.insert(sg.states["fishing"].timeline, TimeEvent(60*FRAMES, function(inst) ChangeAnim(inst) end))    table.insert(sg.states["fishing"].timeline, TimeEvent(80*FRAMES, function(inst) ChangeAnim(inst) end))    table.insert(sg.states["fishing"].timeline, TimeEvent(100*FRAMES, function(inst) ChangeAnim(inst) end))    table.insert(sg.states["fishing"].timeline, TimeEvent(120*FRAMES, function(inst) ChangeAnim(inst) end))    table.insert(sg.states["fishing"].timeline, TimeEvent(140*FRAMES, function(inst) ChangeAnim(inst) end))        table.insert(sg.states["run_start"].timeline, TimeEvent(1*FRAMES, function(inst) ChangeAnim(inst) end))    table.insert(sg.states["run"].timeline, TimeEvent(10*FRAMES, function(inst) ChangeAnim(inst) end))    table.insert(sg.states["run"].timeline, TimeEvent(20*FRAMES, function(inst) ChangeAnim(inst) end))endAddStategraphPostInit("wilson",sg_postinit)AddStategraphPostInit("wilson_client",sg_postinit) -- Haven't tested it in the client yet.

It's more or less the same except this time it hooks into the stategraph. This time it changes the animation as it moves instead changing it only at the start (when the event is received). This also means it's a bit more intensive (still better than DoTaskInTime though). With this the idle animation + rotating camera should work.

I'm still unsure about how well it works with clients. Haven't been able to test it myself.

Also, this time it goes into modmain, put it anywhere inside it.

Share this post


Link to post
Share on other sites
MaiTerra    1

Oh, thanks! It seems to work and doesn't crash my game, so that's good! I'll have to test it out with other people when I get the chance, but this is great!

Share this post


Link to post
Share on other sites
Sabeku    24

@MaiTerra

I was poking around a bit more and figured out how to make idles work:

 

--Remember to replace esctemplate with your character's prefab namelocal function ChangeAnim(inst)    if inst.prefab ~= "esctemplate" or inst:HasTag("playerghost") then return end        local dir = inst.Transform:GetRotation()    local camera_rot = GLOBAL.TheCamera:GetHeadingTarget() % 360        print("Camera rotation:", camera_rot)    print("Direction:", dir)        if camera_rot == 0 or camera_rot == 45 then        if dir <= -5 then            inst.AnimState:SetBuild("esctemplate_right")        else            inst.AnimState:SetBuild("esctemplate_left")        end    elseif camera_rot == 90 or camera_rot == 135 then        if math.abs(dir) >= 90 then            inst.AnimState:SetBuild("esctemplate_right")        else            inst.AnimState:SetBuild("esctemplate_left")        end    elseif camera_rot == 180 or camera_rot == 225 then        if dir >= -5 then            inst.AnimState:SetBuild("esctemplate_right")        else            inst.AnimState:SetBuild("esctemplate_left")        end    else--if camera_rot == 270 or camera_rot == 315 then        if math.abs(dir) <= 90 then            inst.AnimState:SetBuild("esctemplate_right")        else            inst.AnimState:SetBuild("esctemplate_left")        end    endendlocal FRAMES = GLOBAL.FRAMESlocal TimeEvent = GLOBAL.TimeEventlocal function sg_postinit(sg)    local old_locomote_event = sg.events.locomote.fn    sg.events.locomote.fn = function(inst, data)        ChangeAnim(inst)        return old_locomote_event(inst, data)    end        if GLOBAL.TheWorld.ismastersim then        local old_fishingcancel_event = sg.events.fishingcancel.fn        sg.events.fishingcancel.fn = function(inst)            ChangeAnim(inst)            return old_fishingcancel_event(inst)        end    end        -- Add/Remove TimeEvents as you see fitting. More TimeEvents = more accurate animations. Less TimeEvents = Slightly better performance.    table.insert(sg.states["idle"].timeline, TimeEvent(20*FRAMES, function(inst) ChangeAnim(inst) end))    table.insert(sg.states["idle"].timeline, TimeEvent(30*FRAMES, function(inst) ChangeAnim(inst) end))    table.insert(sg.states["idle"].timeline, TimeEvent(40*FRAMES, function(inst) ChangeAnim(inst) end))    table.insert(sg.states["idle"].timeline, TimeEvent(50*FRAMES, function(inst) ChangeAnim(inst) end))    table.insert(sg.states["idle"].timeline, TimeEvent(60*FRAMES, function(inst) ChangeAnim(inst) end))    table.insert(sg.states["idle"].timeline, TimeEvent(70*FRAMES, function(inst) ChangeAnim(inst) end))    table.insert(sg.states["idle"].timeline, TimeEvent(80*FRAMES, function(inst) ChangeAnim(inst) end))    table.insert(sg.states["idle"].timeline, TimeEvent(90*FRAMES, function(inst) ChangeAnim(inst) end))    table.insert(sg.states["idle"].timeline, TimeEvent(100*FRAMES, function(inst) ChangeAnim(inst) end))        if GLOBAL.TheWorld.ismastersim then        table.insert(sg.states["funnyidle"].timeline, TimeEvent(20*FRAMES, function(inst) ChangeAnim(inst) end))        table.insert(sg.states["funnyidle"].timeline, TimeEvent(30*FRAMES, function(inst) ChangeAnim(inst) end))        table.insert(sg.states["funnyidle"].timeline, TimeEvent(40*FRAMES, function(inst) ChangeAnim(inst) end))        table.insert(sg.states["funnyidle"].timeline, TimeEvent(50*FRAMES, function(inst) ChangeAnim(inst) end))        table.insert(sg.states["funnyidle"].timeline, TimeEvent(60*FRAMES, function(inst) ChangeAnim(inst) end))        table.insert(sg.states["funnyidle"].timeline, TimeEvent(70*FRAMES, function(inst) ChangeAnim(inst) end))        table.insert(sg.states["funnyidle"].timeline, TimeEvent(80*FRAMES, function(inst) ChangeAnim(inst) end))    end        table.insert(sg.states["fishing"].timeline, TimeEvent(20*FRAMES, function(inst) ChangeAnim(inst) end))    table.insert(sg.states["fishing"].timeline, TimeEvent(40*FRAMES, function(inst) ChangeAnim(inst) end))    table.insert(sg.states["fishing"].timeline, TimeEvent(60*FRAMES, function(inst) ChangeAnim(inst) end))    table.insert(sg.states["fishing"].timeline, TimeEvent(80*FRAMES, function(inst) ChangeAnim(inst) end))    table.insert(sg.states["fishing"].timeline, TimeEvent(100*FRAMES, function(inst) ChangeAnim(inst) end))    table.insert(sg.states["fishing"].timeline, TimeEvent(120*FRAMES, function(inst) ChangeAnim(inst) end))    table.insert(sg.states["fishing"].timeline, TimeEvent(140*FRAMES, function(inst) ChangeAnim(inst) end))        table.insert(sg.states["run_start"].timeline, TimeEvent(1*FRAMES, function(inst) ChangeAnim(inst) end))    table.insert(sg.states["run"].timeline, TimeEvent(10*FRAMES, function(inst) ChangeAnim(inst) end))    table.insert(sg.states["run"].timeline, TimeEvent(20*FRAMES, function(inst) ChangeAnim(inst) end))endAddStategraphPostInit("wilson",sg_postinit)AddStategraphPostInit("wilson_client",sg_postinit) -- Haven't tested it in the client yet. 

It's more or less the same except this time it hooks into the stategraph. This time it changes the animation as it moves instead changing it only at the start (when the event is received). This also means it's a bit more intensive (still better than DoTaskInTime though). With this the idle animation + rotating camera should work.

I'm still unsure about how well it works with clients. Haven't been able to test it myself.

Also, this time it goes into modmain, put it anywhere inside it. 

 

Thanks a bunch for this code. I haven't been able to test it out yet, but I plan on using it for my character, as they have an asymmetrical face.

It's exactly what I was looking for!

Share this post


Link to post
Share on other sites