Menu
CoddyTech

البطل

جزء من قسم هندسة الألعاب في Unity في رحلة Unity على Coddy. الدرس 50 من 64.

يبني هذا الفصل لعبة Arena Survivor، وهي اللعبة الأخيرة في الدورة. تتدفق موجات الأعداء إلى ساحة محاطة بالجدران، وتقاتلهم بسيفك: جنود مشاة يقتربون منك مشيًا، وكرات تنتظر ثم تنطلق بسرعة. تترك الموجة المهزومة عنصر تعزيز، وتعرض واجهة HUD صحتك والموجة، وعندما تنفد صحتك تنتهي الجولة ويُحفَظ أفضل أداء لك. تستخدم اللعبة أدوات هذا القسم حيثما كان ذلك مناسبًا: عدوًا مجرّدًا Enemy بنوعين، وIDamageable لكل ما يمكن إلحاق الضرر به، والموجات كأصل ScriptableObject، والعناصر الملتقطة خلف واجهة، وواجهة HUD لا تستمع إلا إلى أحداث C#، وحفظًا بصيغة JSON. تبدأ جميع السكربتات الاثني عشر موجودة في المشروع. لا يعمل حتى الآن سوى Hero؛ أما البقية فتحتوي على الحقول التي يحتاج إليها درس لاحق، ويضيف كل درس المزيد.

يضرب سيف البطل ما يقع أمامه، لذا يحتاج التأرجح إلى اختبار يحدد ما إذا كان الهدف «أمامه». تضرب Vector3.Dot(a, b) الأجزاء المتناظرة من متجهين وتجمعها: a.x * b.x + a.y * b.y + a.z * b.z. بالنسبة إلى اتجاهين طولهما 1، تكون النتيجة جيب تمام الزاوية بينهما: 1 عندما يشيران إلى الاتجاه نفسه، و0.866 عند 30 درجة، و0.707 عند 45 درجة، و0 عند الزوايا القائمة، و-1 عندما يشيران إلى اتجاهين متعاكسين. لذا تصبح عبارة «ضمن 30 درجة من اتجاه وجهي» هي «ناتج الضرب النقطي لا يقل عن 0.866»، من دون الحاجة إلى حساب الزاوية. يطلق هذا الروبوت الحارس الإنذار عندما يكون اللاعب على مسافة لا تتجاوز 10 وحدات وداخل مجال رؤيته البالغ 30 درجة:

public Transform player;
public float range = 10f;

void Update()
{
    Vector3 toPlayer = player.position - transform.position;
    toPlayer.y = 0f;
    bool near = toPlayer.magnitude <= range;
    bool inView = Vector3.Dot(transform.forward, toPlayer.normalized) >= 0.866f;
    if (near && inView)
    {
        Debug.Log("Alarm!");
    }
}

يجب أن يكون طول كلا الاتجاهين 1. يكون transform.forward كذلك دائمًا، لكن طول toPlayer يساوي المسافة، لذا يُطبَّع أولًا؛ وإلا فسيعطي اللاعب الذي يبعد ضعف المسافة ناتج ضرب نقطي مضاعفًا. تُضبط قيمة y على 0 قبل ذلك، لذا يُحتسب اللاعب الموجود على ممر مرتفع بحسب موقعه على الخريطة، لا بحسب ارتفاعه. وتُميّز الإشارة الأمام من الخلف: ناتج الضرب النقطي الأكبر من 0 يعني أي موضع في النصف الأمامي، والأقل من 0 يعني أي موضع خلفك.

challenge icon

التحدي

متوسط

امنح البطل سيفًا.

  1. في Update()، عندما يكون مفتاح المسافة أو النقر بزر الفأرة الأيسر مضغوطًا وتكون Time.time >= nextSwing، عيّن nextSwing = Time.time + swingCooldown; واستدعِ Swing().
  2. في Swing()، نفّذ حلقة على Physics.OverlapSphere(transform.position, reach).
  3. دمّر كل عنصر مصاب يحمل الوسم Enemy ويكون اتجاهه الأفقي toHit (مع تعيين y إلى 0) محققًا للشرط Vector3.Dot(transform.forward, toHit.normalized) >= 0.5f.

اضغط «العب» ولوّح بالسيف: ستختفي الدمى التي أمامك. ثم اضغط «تحقق».

جرّب بنفسك

using System.Collections;
using UnityEngine;
using UnityEngine.InputSystem;

public class Hero : MonoBehaviour
{
    [SerializeField] private float speed = 5f;
    [SerializeField] private float reach = 2f;
    [SerializeField] private float swingCooldown = 0.5f;
    [SerializeField] private GameObject slash;

    private CharacterController controller;
    private InputAction moveAction;
    private float nextSwing;

    void Start()
    {
        controller = GetComponent<CharacterController>();
        moveAction = InputSystem.actions.FindAction("Move");
    }

    void Update()
    {
        Vector2 input = moveAction.ReadValue<Vector2>();
        Vector3 move = new Vector3(input.x, 0f, input.y);
        controller.SimpleMove(move * speed);
        if (move != Vector3.zero)
        {
            transform.rotation = Quaternion.LookRotation(move);
        }

        // الضغط على Space أو النقر بزر الفأرة الأيسر ينفّذ ضربة، مرة واحدة على الأكثر كل swingCooldown ثانية
    }

    void Swing()
    {
        StartCoroutine(ShowSlash());
        // دمّر كل ما يحمل الوسم Enemy ويقع ضمن مدى reach وأمام البطل
    }

    IEnumerator ShowSlash()
    {
        slash.SetActive(true);
        yield return new WaitForSeconds(0.1f);
        slash.SetActive(false);
    }
}

جميع دروس هندسة الألعاب في Unity

تدرّب بنفسك: Playground لـ Unity