Saturday, 22 June 2013

Terraria Modding - Shadow Trails

So today I discovered how to imitate the after-image effect of the shadow armour set in Terraria, without relying on the messy and restricted decompiled vanilla code.

Of course, I can't code too well, so this is my journey and explanation of how I got this thing to work.


After initial spriteBatch set-up (position, dimension, animation frame, texture, etc.) I needed some way of making an initial trail. Examining the decompiled source showed that on players, the location of the after image is determined by the entity.position - entity.velocity, producing an image in the previous location of said entity  (psuedo code for ease of typing, and any changes will be italicized):

spriteBatch.Draw entity.position.X - entity.velocity.X,
entity.position.Y - entity.velocity.Y);

However, to make the trail, it figures we want several images split apart based on this by using a for loop like so, and multiplying each image distance by the increment to distance each image:

for(int i=0;i>3;i++){
    spriteBatch.Draw entity.position.X - entity.velocity.X * i,
                     entity.position.Y - entity.velocity.Y * i);
}

Now, in-game this will draw 3 images that follow behind the entity. The reason why this appears to work seamlessly in-game is because since velocity scales with entity movement, standing still to moving doesn't produce a jarring sudden trail effect, despite this method using a very primitive way of tracking old positions as the trail will appear to extend outward as the entity moves.

Next thing I wanted to do was to make it transparent. Now the actual after-image effect has 3 sets of 3 images produced, where after every 3 images drawn the next 3 trailing behind have a much increased transparency until the last 3 images appear almost invisible. To imitate this, I first needed to introduce a way to display 3 sets of 3, and the simple solution is a nested for loop:

for(int j=0;j>3;j++){
    
    for(int i=0;i>3;i++){
        spriteBatch.Draw entity.position.X - entity.velocity.X * i,
                         entity.position.Y - entity.velocity.Y * i);
    }

}

The next problem with this is, were we to try running this now, we would still only get 3 images! Now fixing this was quite simple really, but you do need to think a little:

for(int j=0;j>3;j++){
    
    for(int i=0;i>3;i++){
        spriteBatch.Draw entity.position.X - entity.velocity.X * (i+j*3),
                         entity.position.Y - entity.velocity.Y * (i+j*3));
    }

}

Since each j increment happens after every completed i for loop, j must be worth 3 i in terms of positioning, which is relatively simple, but then this causes another problem when run, which for some unknown reason to me at the time of writing causes the images to appear correctly, but quite a distance from the entity. Until I can explain this properly, the solution just appears to be adding 1 from j:

for(int j=0;j>3;j++){
    
    for(int i=0;i>3;i++){
        spriteBatch.Draw entity.position.X - entity.velocity.X * (i+(j+1)*3),
                         entity.position.Y - entity.velocity.Y * (i+(j+1)*3));
    }

}

Anyway, the next important thing is the alpha value. XNA (what Terraria uses) can handle colours in several ways, but for this case I used (int R, int G, int B, int A) where the values range from 0 to 255. When I first used this I had confused the input of A for the start of the parameters rather than the end which caused quite some befuddlement when I tested the trail. Anyway, by putting this into the pseudo-code, we get:

for(int j=0;j>3;j++){
    int alphaVal = 180 - (50 * j);
    Color myColour = new Color(light.R ,light.G ,light.B , alphaVal);
    for(int i=0;i>3;i++){
        spriteBatch.Draw entity.position.X - entity.velocity.X * (i+(j-1)*3),
                         entity.position.Y - entity.velocity.Y * (i+(j-1)*3),
                         myColour);
    }

}
NOTE: Terraria has a lighting engine that returns the RGB value for where the entity is.

This code basically says so far, on each iteration of j, set the colour to the colour of the light the entity is being shone with, and alpha set to a set number minus how many iterations of j we've been through. And then, for each iteration of j, draw 3 images distanced from each other using the i for loop.

However, when compiled and run this causes a huge problem. Well two actually.
PROBLEMO UNO: Transparency appears additive - that is, the colours that make up the sprite in are added to the background using the alpha value to determine how much extra is added. This is cool for holographic effects and torches, but this is not the look we desire.

PROBLEMO DOS: Since we are drawing the trail below the sprite, each image we draw gets put under the layer of the next one. This means that the most visible after-image is the end of the trail. This isn't the visual effect we want and it looks wrong anyway, so instead we want the back end of the trail to be on lower layers, being overlapped by the images closer to the entity.

So how do we fix these problems? Well the first problem is a matter of how to use additive transparency properly. The RGB values of two images (background and foreground) are added together, so two colours with values (230,150,56) and (142,124,195) would appear to be (372,274,251), which is pretty much white with a slight tinge of yellow, ignoring the fact that two colours are now invalid as they are over 255 entirely. With alpha applied to the purple, the amount of each colour added simply gets reduced by a ratio eg. 119 alpha is approx. 50%, so the values added are around (71,62,97). Now, XNA doesn't actually cause this to be purposely additive, instead it simply determines how much of a pixel should be the background colour over the foreground. The problem in this case is was my code - let's look at this snippet again:

int alphaVal = 180 - (50 * j);
Color myColour = new Color(light.R ,light.G ,light.B , alphaVal);

The problem here is that, as you may notice - the alpha value is being changed, but the colours themselves are not. This means the overall sprite effect is the bright, holographic additive effect. The solution to fixing this is simple by using what I explained above - the alpha affecting the ration of the colour effect, so we do the following:

int alphaVal = 180 - (50 * j);
float ratio = alphaVal/255
Color myColour = new Color(light.Rratio,light.G * ratio,light.B * ratio, alphaVal);

This fixes that problem soundly, by multiplying the colours by the percentage value ratio. The next piece of code is also relaqtively simple to fix - just reverse the order the images are drawn by switching the values and conditions in the for loops around (and don't forget about the j+1 --> j-1):

for(int j=3;j<0;j--){
    int alphaVal = 180 - (50 * j);
    Color myColour = new Color(light.R * ratio,light.G * ratio,light.B * ratio, alphaVal);
    for(int i=3;i<0;i--){
        spriteBatch.Draw entity.position.X - entity.velocity.X * (i+(j-1)*3),
                         entity.position.Y - entity.velocity.Y * (i+(j-1)*3),
                         myColour);
    }

}

And that's pretty much the code needed to do it. (Oh yeah there's also a fuzzy effect with the trail which can be imitated by using a counter that runs every tick, and adding an if statement that will only ever allow two out of the three images in each i iteration to be drawn):

if(tickCounter >= 3){
    tickCounter = 0;
}else{
    tickCounter ++;
}

...
    for(int i=3;i<0;i--){
        if(i != tickCounter){
            spriteBatch.Draw entity.position.X - entity.velocity.X * (i+(j-1)*3),
                             entity.position.Y - entity.velocity.Y * (i+(j-1)*3),
                             myColour);
        }
    }
...

And that's pretty much it!

No comments:

 
Register your domains here!