A couple days ago, I posted another article detailing how I planned to implement the cover system for my game. For the most part, I'm still following it just as it was written, but there have been a couple tweaks, so I want to jot down what those were and discuss where I'm at with it so far!
Check out the video below to see most of the changes I've made in motion before getting into the rest of the article:
Initially, what I'd wanted to do was use a spline component drawn around each "section" of cover (in other words, any walls that are adjacent and that the player should be able to move between without leaving cover). The idea was to store a reference to that component in the Collision Boxes, and use the spline's location to set the pawn's location as they move around cover.
Technically, it worked out ok. I was able to draw the spline and get the pawn to move along it. But there were two main issues: performance and flexibility. Performance because doing that calculation, even only while the player is moving, and ALSO replicating it to other clients was... not impossibly bad, but not great. Flexibility, because you have to draw unique splines for every situation and the distance from the wall isn't always the same either.
Instead, I decided to use some good-old-fashioned vector math. When I realized that using the Arrow Components in the Collision Boxes could double as a normal vector for the wall, it became pretty obvious I could just use a vector perpendicular to any given Arrow Component to limit the player's movement while in cover. It works in circular cover too, provided the boxes are made a bit smaller to keep it looking smooth.
This makes figuring out how to get the character from one wall to an adjacent one smoothly a bit more challenging, but I'm going to start out simple with the solutions. The first thing I'll try is comparing normal vectors each time the player enters a new box, and if they get to one that's different (overlapping two boxes at once), lerping the player's rotation until they are only overlapping one box.
Another idea is to simply lerp the character's location to the new wall once they overlap one of the boxes on the other wall, I just don't like that idea as much because as a player I like to stay in control of my movement as much as possible.
So far, I have the basics of getting in, out, and moving back and forth along a wall of cover. The process of getting into cover basically looks like this:
1.) If the action button is pressed and released within a short time frame, trace for a Cover Collision Box. If we don't find one, we roll. If we do, we cast to it and get the relevant information we need (i.e. location and normal vector from Arrow Component and whether or not it's Low Cover).
The trace is calculated to start at the player location and end about 400cm out in the direction of the player's input. The screenshot below shows the player moving towards cover and pressing the action button, and the green line is the trace for cover:
You have to be careful when moving quickly, because getting into cover is sort of like aiming with the left stick. If you press the wrong way, you'll "misroll". I indicate this using a blue line trace as shown below:
2.) After getting the required data from the Collison Box actor, I use the MoveComponentTo function from the UKismetSystem library to move the pawn's capsule component to the arrow location + a set distance away from the wall. At the same time, a replicated bool called "bCoverSliding" is set to true which activates an animation in the AnimBP:
(I'll take this moment to mention again that there is a video showing what I have so far, which is much more descriptive than screenshots. You can find it here.)
3.) After reaching cover, a callback function is fired. In this function, I set "bCoverSlideCompleted" to true, which effectively puts the player in an "InCover" state and also triggers the Anim BP to put the player in a "Cover Idle" state.
Notice also that if I use the angle of the line trace, I can determine whether the player slid in from the left or right. I can set another replicated boolean called "bTakingCoverRight" based upon the value of this angle to properly set the player rotation to the left when necessary:
The system is fairly flexible, and I activate the "in cover" movement calculations as soon as the player starts sliding, so the animation and movement flow just fine if the player comes in from the left but moves their stick to the right while sliding, for example.
4.) Now that the player is in the "In Cover" state (dictated in code almost entirely by the "bCoverSlideCompleted" variable I mentioned earlier), their input is processed so that they only move along the cover, perpendicular to the wall normal vector. Camera rotation is factored into the calculation as well, so that if the player continued to press "right", for example, as their camera rotates they would move in the direction they are pressing relative to the camera (and stop completely if within a certain angle of inputting movement into the wall).
I wanted to note also that the MoveComponentTo function is a blocking function, in the sense that once you fire it, it will run the callback function no matter what. When implementing the slide cancel, therefore, I had to do a couple of special things:
1.) MoveComponentTo requires one to pass in a Latent Action Info (LAI) struct with certain members defined. To stop the movement from MoveComponentTo, I took advantage of the LAI's UUID member. This value serves as an identifier, and so can be used to stop movement due to an in-progress function.
2.) I used a separate replicated boolean called "bSlideCanceled" which activates if the player cancels the slide (by pushing their input in a direction opposite to the wall). I then use this value in the MoveComponentTo callback function to check whether the player actually hit the wall or not.
By tweaking some of the "easing in" and "easing out" of the slide movement, I also have some "wall bouncing"-like mechanics working in the game as well. It will still require some refinement to feel as good as I'd like, but it is do-able and feels pretty smooth, particularly on a gamepad.
As seen at the end of the video linked above, I have all of this replicating as well, using both a dedicated and listen server model. It's a bit painful going into the details of replication, so I'm not going to get too technical about that aspect. Some of the main points on that front are:
->When replicating movement in cover, we are only concerned about the rotation and animation of the character. Movement is already handled for us by UE4's Character Movement Component. Therefore, I only send Server RPC's when the character stops, starts moving left, or starts moving right in cover.
->The slide animation and state changes are completely covered by replicated booleans in the AnimInstance class which set their values based on the values in the Character Pawn class.
->It is necessary when using the LAI's UUID to cancel a player's movement during a cover slide to also run that on the server. That may seem obvious to anyone familiar with UE4 and the Character Movement Component, but I still think it's worth mentioning.
...And that'll do for now! Things are coming along nicely, so far. I plan on posting again once I have some base logic for moving between adjacent walls, aiming over cover, and vaulting (possibly earlier if any of those warrants more documentation). Until next time!