ID:72820
 
Keywords: resources
[Edited for improved version]
    /* SouthTo8DirIcon()
Takes an icon consisting of only one direction (south) and constructs 7 other directions for it,
essentially turning it into an 8-direction icon that is natively handled by BYOND.
Of course, this is best applied to icons which are displayed in a completely top-down fashion. */


proc/SouthTo8DirIcon()
var/icon/baseIcon = new()
var/icon/changeIcon
var/icon/changeIconDiag

for (var/thisState in icon_states(icon))

changeIcon = icon(icon,thisState)
baseIcon.Insert(changeIcon,icon_state=thisState,dir=SOUTH)
changeIcon.Turn(90)
baseIcon.Insert(changeIcon,icon_state=thisState,dir=WEST)
changeIcon.Turn(90)
baseIcon.Insert(changeIcon,icon_state=thisState,dir=NORTH)
changeIcon.Turn(90)
baseIcon.Insert(changeIcon,icon_state=thisState,dir=EAST)

/*
Some pixelation is inevitable in the case of turning an icon any degree that is not a multiple of 90,
but this can be minimized by performing a slight thickening of the icon. A pixel thicker may not
even be noticed by most players' eyes, but if so you could promote uniformity by also thickening the ones above.
*/


changeIcon.Turn(90) // Return changeIcon to the default facing of south.

// Blending together icon with one shifted a pixel out of place.
changeIcon.Shift(NORTH,1)
changeIconDiag = icon(icon,thisState)
changeIconDiag.Blend(changeIcon,ICON_OVERLAY)

changeIconDiag.Turn(45) // Turn so it's now diagonal, then it's back to business as usual.

baseIcon.Insert(changeIconDiag,icon_state=thisState,dir=SOUTHWEST)
changeIconDiag.Turn(90)
baseIcon.Insert(changeIconDiag,icon_state=thisState,dir=NORTHWEST)
changeIconDiag.Turn(90)
baseIcon.Insert(changeIconDiag,icon_state=thisState,dir=NORTHEAST)
changeIconDiag.Turn(90)
baseIcon.Insert(changeIconDiag,icon_state=thisState,dir=SOUTHEAST)

icon = baseIcon

Seems to work like a charm. This is basically just a real quick way to build mob art assets with only a single direction mob. You can animate them with all the fancy icon_states you want, and it'll go through nicely.

An even more advanced way to do it is just keep a single-facing icon as it is and .turn it whenever the icon facing updates. You should be able to have icons that face in directions far more refined than just the 8 cardinal directions.

However, that is a bit heavier in terms of processing cost - the icons produced by the example code here are pretty much native BYOND handling. An even more efficient way to do it is to construct the icon only once, and then reference or copy them from a master copy every time a new mob of that type comes about.
Heh, that's pretty interesting, never thought of doing that way.

Nice.
Hey, that's pretty nifty!

My only concern is that 45 degree icon rotation often gives less than nice looking results... It's usually alright for a single turn (at least passable), but turning that state again by 45 degrees doesn't generate a state that's the equivalent of a 90 degree turn from the original state; it comes out jagged...

The cardinal directions should probably be generated using 90 degree increments on the original icon_state...

This is definitely a quick method for non-artist developers to generate the necessary art, though! For top-down views only, of course (like your comment mentions)
Turn() isn't perfect. You should be turning from the base icon every time, rather than compounding errors with multiple turns.
Garthor wrote:
Turn() isn't perfect. You should be turning from the base icon every time, rather than compounding errors with multiple turns.

This is true - what I have here is mostly a proof-of-concept thing.

Code that works out a little better looks like this:

 // Pretend that tag works here :P
proc/SouthTo8DirIcon()
var/icon/baseIcon = new()
var/icon/changeIcon
var/icon/changeIconDiag

for (var/thisState in icon_states(icon))

changeIcon = icon(icon,thisState)
baseIcon.Insert(changeIcon,icon_state=thisState,dir=SOUTH)
changeIcon.Turn(90)
baseIcon.Insert(changeIcon,icon_state=thisState,dir=WEST)
changeIcon.Turn(90)
baseIcon.Insert(changeIcon,icon_state=thisState,dir=NORTH)
changeIcon.Turn(90)
baseIcon.Insert(changeIcon,icon_state=thisState,dir=EAST)

// If all you want is 90 degree icons, you can stop right here.
changeIconDiag = icon(icon,thisState)
changeIconDiag.Turn(45)
baseIcon.Insert(changeIconDiag,icon_state=thisState,dir=SOUTHWEST)
changeIconDiag.Turn(90)
baseIcon.Insert(changeIconDiag,icon_state=thisState,dir=NORTHWEST)
changeIconDiag.Turn(90)
baseIcon.Insert(changeIconDiag,icon_state=thisState,dir=NORTHEAST)
changeIconDiag.Turn(90)
baseIcon.Insert(changeIconDiag,icon_state=thisState,dir=SOUTHEAST)

icon = baseIcon


That works relatively well because Turn(90) can preserve the pixels well.

Diagonal does look a little jagged, but I've discovered that this is largely the same if you paste it into a paint program and rotate it 45 degrees too.

The keyword here is that this is a really easy way to do the work that would go into art program manipulation. All that's really missing is some additional beautification -- I wonder if perhaps a bit of tweaking with the icon .Blend() proc (or a similar one) might be able to produce prettier results on the diagonals? [Edit: I discovered that indeed I can by using a 1 pixel offset and ICON_OVERLAY blend.]
A nice procedure, but it could definitely be made more compact:

proc/DirectionalizeIcon(icon/baseIcon, baseDir=SOUTH)

// Use basedir for non-south-facing base icon
var/icon/newIcon=new /icon()
for(var/thisState in icon_states(baseIcon))

for(var/angle in list(0,90,180,270,45,135,225,315))

var/icon/workingIcon=new(baseIcon, thisState)
workingIcon.Turn(angle)
// Turn() turns clockwise, but turn() turns counter-clockwise
// So turn -angle
newIcon.Insert(workingIcon,thisState,turn(baseDir,-angle))

return newIcon

I hope that shows up properly in the comments...is there a way to tag code in comments?
[Edit: Of course it disregards leading whitespace... I've just inserted a blank line between levels of indentation for readability.]

Anyway, I made it a more generalized procedure, but I'm sure there are further possible improvements. Sharing is fun =D
The reason I'm doing it the simple way you see here is to make it easier for newbies to read. Also, I wouldn't roll all 8 directions into the same for loop because of the earlier comments you'll see about how rotating 45 degrees introduces pixel degradation - these directions required special handling.

But you're right that I could definitely make it more compact. I could eliminate at least 9 lines. I don't compact code very often though - I like to poke and prod my code too much to close it up. ;)
I notice now that you're doing some Blend()ing to kind of reinforce the diagonal states, but I based my code off of the version you have in the comments--it does the same thing except from a fresh state each time instead of recycling the same one. The end result should be identical, though my version is not called as a member function. The blending could be worked in pretty easily.

I guess I tend to aim for reduction in my code from the beginning because it forces me to think functionally, and the resulting code is generally more robust. However, I do agree that your code may be more friendly =).
Just a small Q,

mob/verb/test( f as icon )
var/icon/i = new( f )
SouthTo8DirIcon( i )
src << ftp( i, "dump.dmi" )

( with the icon argument defined in the proc )
That should work for a basic test of this proc, right?
The way SouthTo8DirIcon is written here, it doesn't take an icon as an arg, it just generates the icon on the mob (or whatever) the proc is on. It could be rewritten to work that way, though.

Can FTP be used to generate a new DMI file? Not sure, I never tried that. It's one thing for the proc to generate a file of raw data that can be interpreted by FTP, it's another for FTP to be able to intelligently interpret that what it's saving is an icon and so it should save in DMI format. (Of course, if .dmis are in a raw format to begin with, that would be native, but I honestly don't know.)