ID:2019860
 
(See the best response by Ter13.)
After a lengthy absence, I'm returning to making a bump-attack Ys engine. I made a discovery about the system from Ys I&II, and it seems that you take damage if you bump an enemy head-on, but you DON'T take damage if you bump an enemy off-center (basically clipping it form the side).

So, I did some brainstorming and the Bump() proc seems obvious. Now, to bump off-center, I feel I need to use step size 8 so that offset clashes are even possible, but my question is:

What do I call during the Bump proc to make the user bump another mob off-center?
Bump is able to handle pixel movement collisions just fine.
I opted out of pixel movement because it frightens me lol. I decided to just use step size 8 with the classic movement system instead. My troubles come from getting Bump() to differentiate from colliding head-on with an enemy (taking damage), and me bumping the enemy halfway (dealing damage). I'm not sure which variable determines whether I bumped it fully or clipped its side.
If you're using a step size less than or greater than, but not divisible by, world.icon_size you're using pixel movement. Bump() doesn't treat either form differently.
Best response
You will need to know the absolute x or y coordinates of the lesser and greater points of the enemy bounding box to determine this.

To calculate the absolute coordinates:

left: (x-1)*TILE_WIDTH + step_x + bound_x + 1
right: (x-1)*TILE_WIDTH + step_x + bound_x + bound_width
bottom: (y-1)*TILE_HEIGHT + step_y + bound_y + 1
top: (y-1)*TILE_HEIGHT + step_y + bound_y + bound_height


Let's make some shortcuts for this:

#define left_x(ref) ((ref:x-1)*TILE_WIDTH + ref:step_x + ref:bound_x + 1)
#define bottom_y(ref) ((ref:y-1)*TILE_HEIGHT + ref:step_y + ref:bound_y + 1)
#define right_x(ref) ((ref:x-1)*TILE_WIDTH + ref:step_x + ref:bound_x + ref:bound_width)
#define top_y(ref) ((ref:y-1)*TILE_HEIGHT + ref:step_y + ref:bound_y + ref:bound_height)


What we're going to need to do is get the axis that the bump occurs on. If you are allowing diagonal movement, there are three possibilities. If you are not allowing diagonal movement, there are only two. Let's assume that you are are allowing diagonal movement.

In order to correctly determine the axis of the bump, we're going to need to check for the three cases. The only way we can check these three cases is by getting the X and Y deltas of the opposing edges.

//let's just set up a Bump() analogue to make our life easier.
atom
proc
Bumped(atom/movable/bumper)
movable
Bump(atom/obstacle)
obstacle.Bumped(src)
..()


mob
Bumped(atom/movable/bumper)
//get the left and bottom positions of both objects
if(ismob(bumper))
var/this_x1 = left_x(src), this_x2 = this_x1+bound_width-1
var/this_y1 = bottom_y(src), this_y2 = this_y1+bound_height-1
var/that_x1 = left_x(bumper), that_x2 = that_x1+bumper.bound_width-1
var/that_y1 = bottom_y(bumper), that_y2 = that_y1+bumper.bound_height-1
if(this_x2+1==that_x1) //bump from left, facing east
//we need to test that this isn't a diagonal on-corner bump
if(this_y2+1==that_y1) //bump is actually corner-on, facing northeast
//CASE: NORTHEAST
else if(this_y1==that_y2+1)
//CASE: SOUTHEAST
else
//CASE: EAST
else if(this_x1==that_x2+1) //bump from right, facing west
//we need to test that this isn't a diagonal on-corner bump
if(this_y2+1==that_y1) //bump is actually corner-on, facing northwest
//CASE: NORTHWEST
else if(this_y1==that_y2+1) //bump is actually corner-on, facing southwest
//CASE: SOUTHWEST
else //bump is from the side, facing west
//CASE: WEST
else if(this_y2+1==that_y1) //bump from bottom, facing north
//CASE: NORTH
else if(this_y1==that_y2+1) //bump from top, facing south
//CASE: SOUTH
else
//THIS SHOULD NEVER HAPPEN.


What we're doing in the above bit of code is determining what direction the bump is coming from. Because the mob's facing direction isn't entirely reliable for detecting on what part of the bounding box the collision occurred, we have to do this manually. It's fairly ugly. Using trig could result in a much cleaner code structure, but is going to be more computationally expensive than a simple if-else chain.

Now that we've got the collision info, we need to know what to do with it.

Now we're going to compare axial overlap. If the collision is happening on a diagonal case, that's CASE 1. If it's happening on an EAST or WEST case, it's CASE 2. If it's happening on a NORTH or SOUTH case, that's case 3.

Diagonal case, I'm not sure what to do with it, so I'll leave that up to you.

In order to calculate case 2 and case 3, we need to know how much of the player's hitbox should not cross into the enemy's hitbox in order to determine damage.

Let's set this up:

mob
var
hitbox_width = 16
hitbox_height = 16
hitbox_x = 8
hitbox_y = 8


CASE 2 (from EAST/WEST) math:

this_y2 = this_y1 + hitbox_y + hitbox_height - 1
this_y1 += hitbox_y - 1
that_y2 = that_y1 + bumper.hitbox_y + bumper.hitbox_height - 1
that_y1 += bumper.hitbox_y - 1

if(this_y1>=that_y1&&this_y1<=that_y2||this_y2>=that_y1&&this_y2<=that_y2)
//Aligned
else
//Not aligned


CASE 3 (from NORTH/SOUTH) math:

this_x2 = this_x1 + hitbox_x + hitbox_width - 1
this_x1 += hitbox_x - 1
that_x2 = that_x1 + bumper.hitbox_x + bumper.hitbox_width - 1
that_x1 += bumper.hitbox_x - 1

if(this_x1>=that_x1&&this_x1<=that_x2||this_x2>=that_x1&&this_x2<=that_x2)
//Aligned
else
//Not aligned


Now that we know how to set all of this up for each case, let's just put it all together.

#define left_x(ref) ((ref:x-1)*TILE_WIDTH + ref:step_x + ref:bound_x + 1)
#define bottom_y(ref) ((ref:y-1)*TILE_HEIGHT + ref:step_y + ref:bound_y + 1)
#define right_x(ref) ((ref:x-1)*TILE_WIDTH + ref:step_x + ref:bound_x + ref:bound_width)
#define top_y(ref) ((ref:y-1)*TILE_HEIGHT + ref:step_y + ref:bound_y + ref:bound_height)

atom
proc
Bumped(atom/movable/bumper)
movable
Bump(atom/obstacle)
obstacle.Bumped(src)
..()

mob
var
hitbox_width = 16
hitbox_height = 16
hitbox_x = 8
hitbox_y = 8

Bumped(atom/movable/bumper)
//get the left and bottom positions of both objects
if(ismob(bumper))
var/this_x1 = left_x(src), this_x2 = this_x1+bound_width-1
var/this_y1 = bottom_y(src), this_y2 = this_y1+bound_height-1
var/that_x1 = left_x(bumper), that_x2 = that_x1+bumper.bound_width-1
var/that_y1 = bottom_y(bumper), that_y2 = that_y1+bumper.bound_height-1

//get the axis of the hit (step 1)
var/hit_dir = 0
if(this_x2+1==that_x1) //bump from left, facing east
//we need to test that this isn't a diagonal on-corner bump
if(this_y2+1==that_y1) //bump is actually corner-on, facing northeast
hit_dir = NORTHEAST
else if(this_y1==that_y2+1)
hit_dir = SOUTHEAST
else
hit_dir = EAST
else if(this_x1==that_x2+1) //bump from right, facing west
//we need to test that this isn't a diagonal on-corner bump
if(this_y2+1==that_y1) //bump is actually corner-on, facing northwest
hit_dir = NORTHWEST
else if(this_y1==that_y2+1) //bump is actually corner-on, facing southwest
hit_dir = SOUTHWEST
else //bump is from the side, facing west
hit_dir = WEST
else if(this_y2+1==that_y1) //bump from bottom, facing north
hit_dir = NORTH
else if(this_y1==that_y2+1) //bump from top, facing south
hit_dir = SOUTH

if(hit_dir&hit_dir-1) //if this is a diagonal hit
//DO DIAGONAL ATTACK HERE
else if(hit_dir>3) //if this is a horizontal hit
//convert the y bounding box values to hitbox values
this_y2 = this_y1 + hitbox_y + hitbox_height - 1
this_y1 += hitbox_y - 1
that_y2 = that_y1 + bumper.hitbox_y + bumper.hitbox_height - 1
that_y1 += bumper.hitbox_y - 1
//check if the hitboxes are aligned
if(this_y1>=that_y1&&this_y1<=that_y2||this_y2>=that_y1&&this_y2<=that_y2)
//DO ALIGNED ATTACK HERE
else
//DO UNALIGNED ATTACK HERE
else if(hit_dir) //if this is a vertical hit
//convert the x bounding box values to hitbox values
this_x2 = this_x1 + hitbox_x + hitbox_width - 1
this_x1 += hitbox_x - 1
that_x2 = that_x1 + bumper.hitbox_x + bumper.hitbox_width - 1
that_x1 += bumper.hitbox_x - 1
//check if the hitboxes are aligned
if(this_x1>=that_x1&&this_x1<=that_x2||this_x2>=that_x1&&this_x2<=that_x2)
//DO ALIGNED ATTACK HERE
else
//DO UNALIGNED ATTACK HERE


This is not a simple problem to solve. BYOND, unfortunately gives you very little information about collision events. Some of this information is actually fairly difficult to suss out without an extensive knowledge of the underlying structure of how things work. There are probably better ways of going about this, but this is what I managed to come up with in the moment. There are also significant optimizations to this approach available, and there are also more than likely a few things in this approach that will have to be adapted to your specific game's structure.
In response to Chaoder
Chaoder wrote:
I opted out of pixel movement because it frightens me lol. I decided to just use step size 8 with the classic movement system instead. My troubles come from getting Bump() to differentiate from colliding head-on with an enemy (taking damage), and me bumping the enemy halfway (dealing damage). I'm not sure which variable determines whether I bumped it fully or clipped its side.

Ys's combat system will not work with tile-based movement. I'm sorry, but you can't opt out of this one if you want to do what you are trying to do.

The pixel movement system does make come things more difficult (like intelligent AI pathfinding), but it's really something you should probably get used to from the start.
Okay, I see. Thank you for that very elaborate response! I don't even have a game to add it to, luckily. I was hoping to at least get the collision differences down and go from there. I kept playing around with the step size 8 thing and came up with this. It seems to return the appropriate messages depending on whether I hit on-center or not but what do you think?

mob
var
delayatk = 0
step_size = 8
icon = 'TEST.dmi'
Login()
usr.loc = locate(1,1,1,)
usr << sound('Fields Of Battle.mid',1)
Bump(var/mob/Enemy/E)
if (src.delayatk == 0)
if (E.step_y == src.step_y && E.step_x == src.step_x)
src.icon_state = "attacking"
src.delayatk = 1
sleep(2)
src << "You both take damage."
sleep(5)
src.icon_state = ""
src.delayatk = 0
return
else
src.icon_state = "attacking"
src.delayatk = 1
sleep(2)
src << "You deal damage."
sleep(5)
src.icon_state = ""
src.delayatk = 0
return

Enemy
icon_state = "goblin"


Of course, pixel movement would be way cooler, I should delve into it. Is pixel movement too demanding for multiplayer?
Again, you're using pixel movement already unless your world.icon_size is 8, which I doubt.
Sorry, I keep forgetting that, haha. icon's are 32, step size isn't. I'm so used to the mobs moving entire tiles. ^^;
Is pixel movement too demanding for multiplayer?

No.

Of course, pixel movement would be way cooler, I should delve into it.

You cannot implement the combat system you are attempting to implement without pixel movement.

It seems to return the appropriate messages depending on whether I hit on-center or not but what do you think?

Your method most definitely does not work. The response I gave you may not be the very best solution to the problem, but if there was a simpler solution that I was aware of that would work, I would have offered it.

The problem with your solution is that step_x and step_y are values between 0 and tile size - 1 on each axis respectively.

Checking if step_x and step_y are both equal doesn't actually test to see if the bounding boxes are lined up properly, because you aren't accounting for the actual location. There are a large number of cases using this code that will fail, even if it appears to work correctly in some cases.
Okay, thank you. I now understand that I've been using pixel_movement all along in this project, haha. I'll study the code you posted earlier. I'm aware you definitely would've offered something shorter if it were possible, but I'm glad I posted what I tried because I learned why it isn't efficient :D. I forgot to mention, I plan to have the character (and hopefully all non-player mobs) only walk north, south, east and west a la the NES version of Ys I. Does the absence of diagonal movement change anything for better or worse?
Does the absence of diagonal movement change anything for better or worse?

In my code? No. It's safe to use the version that allows diagonal movement even if you don't use it. It would be unsafe the other way, though.

For your game's user experience, though? I'd talk more in depth about Ys I & II and the issues they have, but I think that it would discourage you and I'd like to see you churn out something cool and keep bringing me interesting problems like this one. Keep on chugging, guy.
Thanks your advice! Haha, don't worry. I might know what you mean.