Forum begins after the advertisement:
[Part 3] Recoil problem when player hits the enemy
Home › Forums › Video Game Tutorial Series › Creating a Metroidvania in Unity › [Part 3] Recoil problem when player hits the enemy
- This topic has 4 replies, 3 voices, and was last updated 1 week, 4 days ago by
Terence.
-
AuthorPosts
-
April 9, 2025 at 9:49 pm #17871::
I’m new to game development and I’m having trouble with hits and recoil when adding recoil to a character. Example video:https://www.youtube.com/watch?v=-WqSn5Q-FBI
April 9, 2025 at 9:56 pm #17872::It sounds like you’re looking to implement a recoil system for your character in a game. Here are some general steps and concepts you can follow to achieve this:
1. Understanding Recoil
Recoil is typically the result of a character firing a weapon. It usually involves two main effects:
- Camera movement: The view shifts slightly to simulate the kickback from firing a weapon.
- Character movement: The character may also be pushed back or experience some form of body movement.
2. Implementing Recoil
Here’s a simple way to implement recoil in a game engine:
a. Camera Shake
You can create a camera shake effect when the character fires a weapon. This can be done using a coroutine or a simple timer. Here’s a pseudo-code example:
IEnumerator CameraShake(float duration, float magnitude) { Vector3 originalPosition = Camera.main.transform.localPosition; float elapsed = 0.0f; while (elapsed < duration) { float x = Random.Range(-1f, 1f) * magnitude; float y = Random.Range(-1f, 1f) * magnitude; Camera.main.transform.localPosition = new Vector3(x, y, originalPosition.z); elapsed += Time.deltaTime; yield return null; } Camera.main.transform.localPosition = originalPosition; }
b. Character Recoil
You can apply a force or change the position of your character when shooting. For example:
void ApplyRecoil() { // Adjust these values according to your needs float recoilAmount = 0.1f; Vector3 recoilDirection = new Vector3(0, recoilAmount, 0); // Upwards recoil transform.position += recoilDirection; StartCoroutine(CameraShake(0.1f, 0.1f)); // Call camera shake }
3. Triggering Recoil
You’ll want to call the
ApplyRecoil
method when the player fires their weapon. This would typically be within your shooting logic:void Update() { if (Input.GetButtonDown("Fire1")) // Assuming Fire1 is your shoot button { // Fire the weapon ApplyRecoil(); } }
4. Fine-Tuning
- Adjust the recoil values: Experiment with the magnitude and duration to find what feels right for your game.
- Add additional effects: You could also add sound effects, animations, or particle effects to enhance the shooting experience.
5. Testing
Make sure to test the recoil in different scenarios to ensure it feels natural and not too jarring for the player.
Feel free to ask if you have specific parts of your code you want help with or if you need further clarification on any of these steps!
April 9, 2025 at 10:07 pm #17873::@leovzyabc can you describe the issue in a bit more detail? The recoil in your video looks like it is working. What did you want to change about it?
Do share the scripts for your player and enemy as well.
April 9, 2025 at 10:25 pm #17875::Enemy recoil works but player recoil doesnt work (Player does not move back after attacking) and Even if I don’t attack the enemy, when I hit the enemy it is considered a hit. Player recoil and movement script:
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Animations; public class playermovement : MonoBehaviour { [Header("Horizontal movement settings")] [SerializeField] private float walkspeed = 1; [Space(5)] private float xAxis, yAxis; private Rigidbody2D rb; private bool canDash = true; private bool dashed; [Header("Vertical Movement Settings")] [SerializeField] private float jumpforce = 20; private int jumpBufferCount = 0; [SerializeField] private int jumpbufferFrames; private float coyoTimecounter = 0; [SerializeField] private float coyoteTime; private int airJumpCounter = 0; [SerializeField] private int maxAirJumps; [Space(5)] Animator anim; private float gravity; [Header("Ground check settings")] [SerializeField] private Transform groundcheck; [SerializeField] private float groundcheckY = 0.2f; [SerializeField] private float groundcheckX = 0.5f; [SerializeField] private LayerMask whatIsground; [Space(5)] [Header("Dash Settings")] [SerializeField] private float dashSpeed; [SerializeField] private float dashTime; [SerializeField] private float dashCooldown; [SerializeField] GameObject dashEffect; [Space(5)] [Header("Attacking")] bool attack = false; float timebetweenAttack, timesinceAttack; [SerializeField] Transform SideAttackTransform, UpAttackTranform, Downattacktransfrom; [SerializeField] Vector2 SideAttackArea,UpAttackArea,DownAttackArea; [SerializeField] LayerMask attackableLayer; [SerializeField] float damage; [Space(5)] [Header("Recoil")] [SerializeField] int recoilXsteps = 5; [SerializeField] int recoilYsteps = 5; [SerializeField] float recoilXspeed = 100; [SerializeField] float recoilYspeed = 100; int stepsXrecoiled, stepsYrecoiled; [Space(5)] PlayerstateList pState; public static playermovement Instance; private void Awake() { if(Instance != null && Instance != this) { Destroy(gameObject); } else { Instance = this; } } void Start() { pState = GetComponent<PlayerstateList>(); rb = GetComponent<Rigidbody2D>(); anim = GetComponent<Animator>(); gravity = rb.gravityScale; } private void ODrawGizmos() { Gizmos.color = Color.red; Gizmos.DrawWireCube(SideAttackTransform.position, SideAttackArea); Gizmos.DrawWireCube(UpAttackTranform.position, UpAttackArea); Gizmos.DrawWireCube(Downattacktransfrom.position, DownAttackArea); } void Update() { GetInputs(); UpdateJumpVariables(); startDash(); if (pState.dashing) return; Flip(); Move(); Jump(); Attack(); } void GetInputs() { xAxis = Input.GetAxisRaw("Horizontal"); yAxis = Input.GetAxisRaw("Vertical"); attack = Input.GetMouseButtonDown(0); } void Flip () { if(xAxis < 0) { transform.localScale = new Vector2(-1, transform.localScale.y); pState.lookingRight = false; } else if(xAxis > 0 ) { transform.localScale = new Vector2(1, transform.localScale.y); pState.lookingRight = true; } } private void Move() { rb.velocity = new Vector2(walkspeed * xAxis, rb.velocity.y); anim.SetBool("Walking", rb.velocity.x != 0 && grounded()); } void startDash() { if(Input.GetButtonDown("Dash") && canDash && !dashed) { StartCoroutine(Dash()); dashed = true; } if (grounded()) { dashed = false; } } IEnumerator Dash() { canDash = false; pState.dashing = true; anim.SetTrigger("Dashing"); rb.velocity = new Vector2(transform. localScale.x * dashSpeed, 0); if(grounded()) Instantiate(dashEffect, transform); rb.gravityScale = 0; yield return new WaitForSeconds(dashTime); rb.gravityScale = gravity; pState.dashing = false; yield return new WaitForSeconds(dashCooldown); canDash = true; } void Attack() { timesinceAttack += Time.deltaTime; if (attack && timesinceAttack >= timebetweenAttack) { timesinceAttack = 0; anim.SetTrigger("Attacking"); if(yAxis == 0 || yAxis < 0 && grounded()) { Hit(SideAttackTransform, SideAttackArea, ref pState.recoilingX, recoilXspeed); } else if(yAxis > 0) { Hit(UpAttackTranform, UpAttackArea, ref pState.recoilingY, recoilYspeed); } else if(yAxis > 0 && !grounded()) { Hit(Downattacktransfrom, DownAttackArea, ref pState.recoilingY, recoilYspeed); } } } void Hit(Transform _attackTransform, Vector2 _attackArea, ref bool _recoilDir, float _recoilStrength) { Collider2D[] objectsToHit = Physics2D.OverlapBoxAll(_attackTransform.position, _attackArea, 0, attackableLayer); if(objectsToHit.Length > 0) { _recoilDir = true; } for(int i = 0; i < objectsToHit.Length; i++) { if (objectsToHit[i].GetComponent<Enemy>() != null) { objectsToHit[i].GetComponent<Enemy>().enemyHit(damage, (transform.position - objectsToHit[i].transform.position).normalized, _recoilStrength); } } } void Recoil() { if(pState.recoilingX) { if(pState.lookingRight) { rb.velocity = new Vector2 (-recoilXspeed, 0); } else { rb.velocity = new Vector2 (recoilXspeed, 0); } } if(pState.recoilingY) { rb.gravityScale = 0; if(yAxis <0) { rb.velocity = new Vector2(rb.velocity.x, recoilYspeed); } else { rb.velocity = new Vector2(rb.velocity.x, -recoilYspeed); } airJumpCounter = 0; } else { rb.gravityScale = gravity; } //stop recoil if(pState.recoilingX && stepsXrecoiled < recoilXsteps) { stepsXrecoiled++; } else { StopRecoilX(); } if(pState.recoilingY && stepsYrecoiled < recoilYsteps) { stepsYrecoiled++; } else { StopRecoilY(); } if(grounded()) { StopRecoilY(); } } void StopRecoilX() { stepsXrecoiled = 0; pState.recoilingX = false; } void StopRecoilY() { stepsYrecoiled = 0; pState.recoilingY = false; } public bool grounded() { if(Physics2D.Raycast(groundcheck.position, Vector2.down, groundcheckY, whatIsground) || Physics2D.Raycast(groundcheck.position + new Vector3(groundcheckX,0, 0), Vector2.down, groundcheckY, whatIsground) || Physics2D.Raycast(groundcheck.position + new Vector3(-groundcheckX,0, 0), Vector2.down, groundcheckY, whatIsground) ) { return true; } else { return false; } } void Jump() { if(Input.GetButtonUp("Jump") && rb.velocity.y > 0) { rb.velocity = new Vector2(rb.velocity.x, 0); pState.jumping = false; } if(!pState.jumping) { if(jumpBufferCount > 0 && coyoTimecounter > 0) { rb.velocity = new Vector3(rb.velocity.x, jumpforce); pState.jumping = true; } else if(!grounded() && airJumpCounter < maxAirJumps && Input.GetButtonDown("Jump")) { pState.jumping = true; airJumpCounter++; rb.velocity = new Vector3(rb.velocity.x, jumpforce); } } anim.SetBool("Jumping", !grounded()); } void UpdateJumpVariables() { if(grounded()) { pState.jumping = false; coyoTimecounter = coyoteTime; airJumpCounter = 0; } else { coyoTimecounter -= Time.deltaTime; } if (Input.GetButtonDown("Jump")) { jumpBufferCount = jumpbufferFrames; } else { jumpBufferCount--; } } }
Enemy script:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Enemy : MonoBehaviour { [SerializeField] float health; [SerializeField] float recoilLength; [SerializeField] float recoilFactor; [SerializeField] bool isRecoiling = false; float recoilTimer; Rigidbody2D rb; void Start() { } void Awake() { rb = GetComponent<Rigidbody2D>(); } void Update() { if(health <= 0) { Destroy(gameObject); } if(isRecoiling) { if(recoilTimer < recoilLength) { recoilTimer+= Time.deltaTime; } else { isRecoiling = false; recoilTimer = 0; } } } public void enemyHit(float _damageDone, Vector2 _hitDirection, float _hitForce) { health -= _damageDone; if (!isRecoiling) { rb.AddForce(-_hitForce * recoilFactor * _hitDirection); } } }
April 9, 2025 at 10:31 pm #17876::To address the issues you’re facing with player recoil and unintended hits on the enemy, consider the following points:
-
Player Recoil Not Working: Ensure that you are calling the
Recoil()
method in yourUpdate()
function after theAttack()
method. It seems you have implemented the recoil logic, but it may not be executed ifRecoil()
is not called. AddRecoil();
at the end of yourUpdate()
method. -
Unintended Hits on Enemy: The condition to set
_recoilDir
totrue
in theHit()
method is currently based on whether any objects are detected in the attack area. This means that even if the player attacks without hitting an enemy, it will still register a hit. You should only set_recoilDir
totrue
if the player actually hits an enemy. Move the line_recoilDir = true;
inside the loop that checks for enemies:for(int i = 0; i < objectsToHit.Length; i++) { if (objectsToHit[i].GetComponent() != null) { _recoilDir = true; // Move this line here objectsToHit[i].GetComponent().enemyHit(damage, (transform.position - objectsToHit[i].transform.position).normalized, _recoilStrength); } }
By making these adjustments, you should see the player recoil correctly after attacking and ensure that hits are only registered when an enemy is actually hit. If you have further questions or need additional clarification, feel free to ask!
April 10, 2025 at 8:12 pm #17879::Check out this post here. Make the same change on your end and the recoil should start working for you: https://blog.terresquall.com/community/topic/part-3-recoil-will-still-not-work-no-matter-what-i-try/#post-17525
-
AuthorPosts
- You must be logged in to reply to this topic.
Advertisement below: