2D RPG Accelerator : Release 1.0.2

Deniz TrakaDeniz Traka
7 min read

This includes bunch of new features to the project. New weapon types including ranged (yes Bows!), looting enemies and cool attack/damage effects to give more satisfaction from the hits!

What do we have in table ?

Yes, before we start explaining how is everything made up from technical side to the front pixel graphics, lets list them one by one.

With the latest release of the project which is release-1.0.2, I added such nice features like below;

  • Two Handed Weapons

  • Ranged Weapons

  • Offhand equipments - Shields!

  • Hit effect

  • Weapon swing effects

  • Basic dungeon tile sets and map itself.

Lets check how I implemented all of them and included in project so everyone can use and extend to align with their projects.

Get it all and more on Unity Asset Store Page!

1- TwoHanded Weapons

Actually those are simple weapons inherited from BaseMeleeWeapon class as for the other weapon types. But I just added extra field to ‘WeaponData’ class to be able to check if the weapon can be equipped on that moment or do I need to unequip the items before equipping this.

Actually those are simple weapons inherited from BaseMeleeWeapon class as for the other weapon types. But I just added extra field toWeaponDataclass to be able to check if the weapon can be equipped on that moment or do I need to unequip the items before equipping this.using UnityEngine;

namespace DTWorldz.Items.Data.Scriptables
{
    [CreateAssetMenu(fileName = "NewWeaponData", menuName = "Items/WeaponData")]
    public class WeaponData : EquipmentData
    {
        public int Damage;
        public float Speed;
        public float Range;
        public bool IsTwoHanded;

        public float StaminaCost; // Stamina cost to use the weapon
        public AudioClip AttackSound; // Sound played when the weapon is used
        public AudioClip SwingSound; // Sound played when the weapon is swung
        public GameObject HitEffect; // Particle effect to play when the weapon hits something
    }
}

Dead simple, isn’t it?

The only thing left to DEV is to assign weapon as two handed while configuring the weapon asset as below.

and yes.. your two handed weapon is ready. I simply didn’t want to put effort to draw another bunch of frames just for two handed weapons since functionality is there and I think it looks pretty good.

2- Ranged Weapons

Actually ranged weapons required some updates to handle things nicely.
I wanted to make it realistic. So to be able to do that, I wanted to handle the damage directly given by arrow itself with the extra bonus from character’s equipped bow strength and dexterity.
That means creating new item which is Arrow in this case, and initiating it on ranged weapon perform attack function.
Arrow is simply basic item but it’s mediator will handle the Triggering movement force of the arrow to the target, and it’s ItemBehaviour will handle the OnHit event. event as below.

using DTWorldz.Behaviours.Mobiles;
using DTWorldz.Items.Models.Equipments.MainHand.Weapons.Ranged;
using DTWorldz.Items.Models.Projectiles;
using UnityEngine;

namespace DTWorldz.Items.Behaviours.Projectiles
{
    public class ArrowMediator : BaseProjectileMediator<BaseProjectile, BaseProjectileBehaviour<BaseProjectile>, BaseProjectileAudioBehavior<BaseProjectile>, BaseProjectileParticleEffectBehaviour<BaseProjectile>>
    {
        public override void Awake()
        {
            gameObject.AddComponent<ArrowAudioBehavior>();
            gameObject.AddComponent<ArrowParticleEffectBehaviour>();
            gameObject.AddComponent<ArrowBehaviour>();
            base.Awake();
        }

        public override void Trigger(MobileBehaviour from, MobileBehaviour target, BaseRangedWeapon weapon)
        {
            ItemBehaviour.Init(from, target, weapon);
            this.Target = target;

            var actualTargetPosition = new Vector3(target.transform.position.x, target.transform.position.y + 0.75f, 0);

            // Calculate the direction to the target
            Vector2 direction = (actualTargetPosition - transform.position).normalized;

            // Rotate the projectile to face the target
            float angle = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg;
            transform.rotation = Quaternion.Euler(0, 0, angle);

            // Apply force in the direction of the target
            GetComponent<Rigidbody2D>().AddForce(direction * weapon.Speed*10, ForceMode2D.Impulse);
            // Destroy(gameObject, weapon.ProjectileLifeTime);

        }
    }
}
using System;
using DTWorldz.Behaviours.Mobiles;
using DTWorldz.Behaviours.ObjectPools;
using DTWorldz.Helpers;
using DTWorldz.Items.Models;
using DTWorldz.Items.Models.Equipments.MainHand.Weapons.Ranged;
using DTWorldz.Items.Models.Projectiles;
using Unity.VisualScripting;
using UnityEngine;

namespace DTWorldz.Items.Behaviours.Projectiles
{
    public abstract class BaseProjectileBehaviour<T> : BaseItemBehaviour<T> where T : BaseProjectile
    {
        public Action OnHit;
        private bool triggered;
        private MobileBehaviour from;
        private MobileBehaviour targetMobile;
        private float lifeTime;
        private BaseRangedWeapon weapon;

        public override void Initialize(T itemData)
        {
            base.Initialize(itemData);
            lifeTime = itemData.LifeTime;
            if(triggered){
                // decrease the projectile count in the inventory
                // if the projectile is triggered, remove it from the inventory
                var slotIndex = from.InventoryBehaviour.FindItemSlotIndex(BaseItem.ID);
                if(slotIndex != -1){
                    from.InventoryBehaviour.RemoveItem(BaseItem, 1, slotIndex);
                }
                Destroy(gameObject, lifeTime);
            }
        }

        // Initialize the projectile with required information to calculate the damage and handle the collision
        internal void Init(MobileBehaviour from, MobileBehaviour target, BaseRangedWeapon weapon)
        {
            this.triggered = true;
            this.from = from;
            this.targetMobile = target;
            this.weapon = weapon;
        }

        // handle collision with the target
        private void OnTriggerEnter2D(Collider2D collision)
        {
            // Check if the object collided is a MobileBehaviour
            MobileBehaviour target = collision.GetComponent<MobileBehaviour>();
            if (target != null && target != from && target == targetMobile)
            {
                // add randomness to hit
                var random = new System.Random();
                var hitChance = random.Next(0, 100);
                if(hitChance > 25){
                    OnHit?.Invoke();
                    var damage = DamageHelper.CalculateProjectileDamage<T>(from, target, weapon, BaseItem);
                    targetMobile.Mobile.TakeDamage(from.Mobile, damage);
                    FloatingDamageTextPool.Instance.ShowDamageText(damage, targetMobile.transform.position, targetMobile.transform);
                }
            }
        }

        public override void OnMouseEnter()
        {
            // show only if the projectile is not triggered, which means it's just an item in the inventory or on the ground
            if (triggered)
            {
                return;
            }
            base.OnMouseEnter();
        }

        public override void OnMouseExit()
        {
            // show only if the projectile is not triggered, which means it's just an item in the inventory or on the ground
            if (triggered)
            {
                return;
            }
            base.OnMouseExit();
        }
    }
}

With this setup, our weapon is ready to handle ranged attacks..

This is how to handle differentiate attack types on PerformAttack() function.

if (CheckAttack(target, weapon))
            {
                SoundUtils.PlayDetachedSound(weapon.SwingSound);

                if (weapon is BaseMeleeWeapon)
                {
                    // we take the damage if melee weapon
                    var meleeDamage = Mobile.Attack(target.Mobile);
                    if (meleeDamage > -1)
                    {
                        HandleMeleeHit(weapon, target, meleeDamage);
                    }
                }
                else if (weapon is BaseRangedWeapon rangedWeapon)
                {
                    Mobile.InvokeRangedAttack(target.Mobile, rangedWeapon);
                    var cooldown = mobile.GetAttackCooldown();
                    StartCoroutine(ExecuteAfterTimeRanged(cooldown / 2, InitiateProjectile, rangedWeapon, target));
                }
            }
        }

        private void InitiateProjectile(BaseRangedWeapon weapon, MobileBehaviour target)
        {
            var initPosition = new Vector3(transform.position.x, transform.position.y + 0.75f, transform.position.z);
            // first instantiate the projectile
            var projectile = Instantiate(weapon.ProjectilePrefab, initPosition, Quaternion.identity);
            var projectileBehaviour = projectile.GetComponent<IProjectile>();
            projectileBehaviour.Trigger(this, target, weapon);

        }

3- Shields!

Actually there is nothing to explain about this.
Implementation was already there,
I just needed to create and item and needed to render it in the correct rendering slot and that’s it :)

But looking quite nice!

4- Weapon Swing and Hit Effects

This is where I just used Object Pool and Singletons actually.
Whenever character makes the hit, i instantiate the hitEffect and bloodstain on the position where the target mobile is.

There is a generic object pool class which works with any object.

using System.Collections.Generic;
using UnityEngine;

namespace DTWorldz.Behaviours.ObjectPools
{
    public class ObjectPool<T> where T : Component
    {
        private List<T> prefabs;
        private readonly Queue<T> objects = new Queue<T>();
        private Transform parentTransform;

        public void InitPool(List<T> prefabs, int initialSize = 10, Transform parent = null)
        {
            this.prefabs = prefabs;
            this.parentTransform = parent;

            // Pre-populate the pool
            for (int i = 0; i < initialSize; i++)
            {
                T obj = CreateNewObject();
                objects.Enqueue(obj);
                obj.gameObject.SetActive(false);
            }
        }

        private T CreateNewObject()
        {
            // get random from prefabs
            T prefab = prefabs[Random.Range(0, prefabs.Count)];
            T newObj = Object.Instantiate(prefab, parentTransform);
            newObj.gameObject.SetActive(false);
            return newObj;
        }

        public T Get()
        {
            if (objects.Count > 0)
            {
                T obj = objects.Dequeue();
                obj.gameObject.SetActive(true);
                return obj;
            }
            else
            {
                return CreateNewObject(); // Expand pool if empty.
            }
        }

        public void ReturnToPool(T obj)
        {
            obj.gameObject.SetActive(false);
            objects.Enqueue(obj);
        }
    }
}

But to be able to use it you should put your own type of object pool ofcourse.

using System.Collections.Generic;
using UnityEngine;

namespace DTWorldz.Behaviours.ObjectPools
{
    public class BloodStainPool : MonoBehaviour
    {
        public int Size = 25;
        public static BloodStainPool Instance { get; private set; }

        [SerializeField] private List<BloodStainBehaviour> BloodStainPrefabs;

        private ObjectPool<BloodStainBehaviour> pool;

        private void Awake()
        {
            if (Instance == null)
            {
                Instance = this;
            }
            else
            {
                Destroy(gameObject);  // Only works in MonoBehaviour
                return;
            }

            // Initialize the pool with the prefab
            pool = new ObjectPool<BloodStainBehaviour>();
            pool.InitPool(BloodStainPrefabs, Size, transform);
        }

        public void Create(Vector3 worldPosition)
        {
            if (1 > UnityEngine.Random.Range(0, 100)){
                return;
            }

            worldPosition.x += UnityEngine.Random.Range(-0.5f, 0.5f);
            worldPosition.y += UnityEngine.Random.Range(-0.5f, 0.5f);

            // Get a damage text object from the pool
            BloodStainBehaviour bloodStain = pool.Get();
            bloodStain.transform.position = worldPosition;
            bloodStain.Initialize();

        }

        public void ReturnToPool(BloodStainBehaviour bloodStain)
        {
            pool.ReturnToPool(bloodStain);
        }
    }
}

That is simply it!

The weapon swing effects simply added directly to spritesheet of each weapon.

5- Basic Tile Map

I am adding the sprite sheets directly below here so you can get it freely. :)

0
Subscribe to my newsletter

Read articles from Deniz Traka directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Deniz Traka
Deniz Traka