קידוד מערכת מלאי פשוטה עם גרירה ושחרור ממשק משתמש ב-Unity

משחקים רבים מאפשרים לשחקנים לאסוף ולסחוב מספר רב של פריטים (למשל משחקי RTS/MOBA/RPG, משחקי תפקידים פעולה וכו'), שם ה-Inventory נכנס לתמונה.

Inventory הוא טבלת אלמנטים המספקת גישה מהירה לפריטי שחקן ודרך פשוטה לארגן אותם.

מערכת מלאי דיאבלו 3

בפוסט זה, נלמד כיצד לתכנת מערכת מלאי פשוטה עם איסוף פריטים וגרירה ושחרור ממשק משתמש ב-Unity.

שלב 1: צור את הסקריפטים

מדריך זה דורש 3 סקריפטים:

SC_CharacterController.cs

//You are free to use this script in Free or Commercial projects
//sharpcoderblog.com @2019

using UnityEngine;

[RequireComponent(typeof(CharacterController))]

public class SC_CharacterController : MonoBehaviour
{
    public float speed = 7.5f;
    public float jumpSpeed = 8.0f;
    public float gravity = 20.0f;
    public Camera playerCamera;
    public float lookSpeed = 2.0f;
    public float lookXLimit = 60.0f;

    CharacterController characterController;
    Vector3 moveDirection = Vector3.zero;
    Vector2 rotation = Vector2.zero;

    [HideInInspector]
    public bool canMove = true;

    void Start()
    {
        characterController = GetComponent<CharacterController>();
        rotation.y = transform.eulerAngles.y;
    }

    void Update()
    {
        if (characterController.isGrounded)
        {
            // We are grounded, so recalculate move direction based on axes
            Vector3 forward = transform.TransformDirection(Vector3.forward);
            Vector3 right = transform.TransformDirection(Vector3.right);
            float curSpeedX = speed * Input.GetAxis("Vertical");
            float curSpeedY = speed * Input.GetAxis("Horizontal");
            moveDirection = (forward * curSpeedX) + (right * curSpeedY);

            if (Input.GetButton("Jump"))
            {
                moveDirection.y = jumpSpeed;
            }
        }

        // Apply gravity. Gravity is multiplied by deltaTime twice (once here, and once below
        // when the moveDirection is multiplied by deltaTime). This is because gravity should be applied
        // as an acceleration (ms^-2)
        moveDirection.y -= gravity * Time.deltaTime;

        // Move the controller
        characterController.Move(moveDirection * Time.deltaTime);

        // Player and Camera rotation
        if (canMove)
        {
            rotation.y += Input.GetAxis("Mouse X") * lookSpeed;
            rotation.x += -Input.GetAxis("Mouse Y") * lookSpeed;
            rotation.x = Mathf.Clamp(rotation.x, -lookXLimit, lookXLimit);
            playerCamera.transform.localRotation = Quaternion.Euler(rotation.x, 0, 0);
            transform.eulerAngles = new Vector2(0, rotation.y);
        }
    }
}

SC_PickItem.cs

//You are free to use this script in Free or Commercial projects
//sharpcoderblog.com @2019

using UnityEngine;

public class SC_PickItem : MonoBehaviour
{
    public string itemName = "Some Item"; //Each item must have an unique name
    public Texture itemPreview;

    void Start()
    {
        //Change item tag to Respawn to detect when we look at it
        gameObject.tag = "Respawn";
    }

    public void PickItem()
    {
        Destroy(gameObject);
    }
}

SC_InventorySystem.cs

//You are free to use this script in Free or Commercial projects
//sharpcoderblog.com @2019

using UnityEngine;

public class SC_InventorySystem : MonoBehaviour
{
    public Texture crosshairTexture;
    public SC_CharacterController playerController;
    public SC_PickItem[] availableItems; //List with Prefabs of all the available items

    //Available items slots
    int[] itemSlots = new int[12];
    bool showInventory = false;
    float windowAnimation = 1;
    float animationTimer = 0;

    //UI Drag & Drop
    int hoveringOverIndex = -1;
    int itemIndexToDrag = -1;
    Vector2 dragOffset = Vector2.zero;

    //Item Pick up
    SC_PickItem detectedItem;
    int detectedItemIndex;

    // Start is called before the first frame update
    void Start()
    {
        Cursor.visible = false;
        Cursor.lockState = CursorLockMode.Locked;

        //Initialize Item Slots
        for (int i = 0; i < itemSlots.Length; i++)
        {
            itemSlots[i] = -1;
        }
    }

    // Update is called once per frame
    void Update()
    {
        //Show/Hide inventory
        if (Input.GetKeyDown(KeyCode.Tab))
        {
            showInventory = !showInventory;
            animationTimer = 0;

            if (showInventory)
            {
                Cursor.visible = true;
                Cursor.lockState = CursorLockMode.None;
            }
            else
            {
                Cursor.visible = false;
                Cursor.lockState = CursorLockMode.Locked;
            }
        }

        if (animationTimer < 1)
        {
            animationTimer += Time.deltaTime;
        }

        if (showInventory)
        {
            windowAnimation = Mathf.Lerp(windowAnimation, 0, animationTimer);
            playerController.canMove = false;
        }
        else
        {
            windowAnimation = Mathf.Lerp(windowAnimation, 1f, animationTimer);
            playerController.canMove = true;
        }

        //Begin item drag
        if (Input.GetMouseButtonDown(0) && hoveringOverIndex > -1 && itemSlots[hoveringOverIndex] > -1)
        {
            itemIndexToDrag = hoveringOverIndex;
        }

        //Release dragged item
        if (Input.GetMouseButtonUp(0) && itemIndexToDrag > -1)
        {
            if (hoveringOverIndex < 0)
            {
                //Drop the item outside
                Instantiate(availableItems[itemSlots[itemIndexToDrag]], playerController.playerCamera.transform.position + (playerController.playerCamera.transform.forward), Quaternion.identity);
                itemSlots[itemIndexToDrag] = -1;
            }
            else
            {
                //Switch items between the selected slot and the one we are hovering on
                int itemIndexTmp = itemSlots[itemIndexToDrag];
                itemSlots[itemIndexToDrag] = itemSlots[hoveringOverIndex];
                itemSlots[hoveringOverIndex] = itemIndexTmp;

            }
            itemIndexToDrag = -1;
        }

        //Item pick up
        if (detectedItem && detectedItemIndex > -1)
        {
            if (Input.GetKeyDown(KeyCode.F))
            {
                //Add the item to inventory
                int slotToAddTo = -1;
                for (int i = 0; i < itemSlots.Length; i++)
                {
                    if (itemSlots[i] == -1)
                    {
                        slotToAddTo = i;
                        break;
                    }
                }
                if (slotToAddTo > -1)
                {
                    itemSlots[slotToAddTo] = detectedItemIndex;
                    detectedItem.PickItem();
                }
            }
        }
    }

    void FixedUpdate()
    {
        //Detect if the Player is looking at any item
        RaycastHit hit;
        Ray ray = playerController.playerCamera.ViewportPointToRay(new Vector3(0.5F, 0.5F, 0));

        if (Physics.Raycast(ray, out hit, 2.5f))
        {
            Transform objectHit = hit.transform;

            if (objectHit.CompareTag("Respawn"))
            {
                if ((detectedItem == null || detectedItem.transform != objectHit) && objectHit.GetComponent<SC_PickItem>() != null)
                {
                    SC_PickItem itemTmp = objectHit.GetComponent<SC_PickItem>();

                    //Check if item is in availableItemsList
                    for (int i = 0; i < availableItems.Length; i++)
                    {
                        if (availableItems[i].itemName == itemTmp.itemName)
                        {
                            detectedItem = itemTmp;
                            detectedItemIndex = i;
                        }
                    }
                }
            }
            else
            {
                detectedItem = null;
            }
        }
        else
        {
            detectedItem = null;
        }
    }

    void OnGUI()
    {
        //Inventory UI
        GUI.Label(new Rect(5, 5, 200, 25), "Press 'Tab' to open Inventory");

        //Inventory window
        if (windowAnimation < 1)
        {
            GUILayout.BeginArea(new Rect(10 - (430 * windowAnimation), Screen.height / 2 - 200, 302, 430), GUI.skin.GetStyle("box"));

            GUILayout.Label("Inventory", GUILayout.Height(25));

            GUILayout.BeginVertical();
            for (int i = 0; i < itemSlots.Length; i += 3)
            {
                GUILayout.BeginHorizontal();
                //Display 3 items in a row
                for (int a = 0; a < 3; a++)
                {
                    if (i + a < itemSlots.Length)
                    {
                        if (itemIndexToDrag == i + a || (itemIndexToDrag > -1 && hoveringOverIndex == i + a))
                        {
                            GUI.enabled = false;
                        }

                        if (itemSlots[i + a] > -1)
                        {
                            if (availableItems[itemSlots[i + a]].itemPreview)
                            {
                                GUILayout.Box(availableItems[itemSlots[i + a]].itemPreview, GUILayout.Width(95), GUILayout.Height(95));
                            }
                            else
                            {
                                GUILayout.Box(availableItems[itemSlots[i + a]].itemName, GUILayout.Width(95), GUILayout.Height(95));
                            }
                        }
                        else
                        {
                            //Empty slot
                            GUILayout.Box("", GUILayout.Width(95), GUILayout.Height(95));
                        }

                        //Detect if the mouse cursor is hovering over item
                        Rect lastRect = GUILayoutUtility.GetLastRect();
                        Vector2 eventMousePositon = Event.current.mousePosition;
                        if (Event.current.type == EventType.Repaint && lastRect.Contains(eventMousePositon))
                        {
                            hoveringOverIndex = i + a;
                            if (itemIndexToDrag < 0)
                            {
                                dragOffset = new Vector2(lastRect.x - eventMousePositon.x, lastRect.y - eventMousePositon.y);
                            }
                        }

                        GUI.enabled = true;
                    }
                }
                GUILayout.EndHorizontal();
            }
            GUILayout.EndVertical();

            if (Event.current.type == EventType.Repaint && !GUILayoutUtility.GetLastRect().Contains(Event.current.mousePosition))
            {
                hoveringOverIndex = -1;
            }

            GUILayout.EndArea();
        }

        //Item dragging
        if (itemIndexToDrag > -1)
        {
            if (availableItems[itemSlots[itemIndexToDrag]].itemPreview)
            {
                GUI.Box(new Rect(Input.mousePosition.x + dragOffset.x, Screen.height - Input.mousePosition.y + dragOffset.y, 95, 95), availableItems[itemSlots[itemIndexToDrag]].itemPreview);
            }
            else
            {
                GUI.Box(new Rect(Input.mousePosition.x + dragOffset.x, Screen.height - Input.mousePosition.y + dragOffset.y, 95, 95), availableItems[itemSlots[itemIndexToDrag]].itemName);
            }
        }

        //Display item name when hovering over it
        if (hoveringOverIndex > -1 && itemSlots[hoveringOverIndex] > -1 && itemIndexToDrag < 0)
        {
            GUI.Box(new Rect(Input.mousePosition.x, Screen.height - Input.mousePosition.y - 30, 100, 25), availableItems[itemSlots[hoveringOverIndex]].itemName);
        }

        if (!showInventory)
        {
            //Player crosshair
            GUI.color = detectedItem ? Color.green : Color.white;
            GUI.DrawTexture(new Rect(Screen.width / 2 - 4, Screen.height / 2 - 4, 8, 8), crosshairTexture);
            GUI.color = Color.white;

            //Pick up message
            if (detectedItem)
            {
                GUI.color = new Color(0, 0, 0, 0.84f);
                GUI.Label(new Rect(Screen.width / 2 - 75 + 1, Screen.height / 2 - 50 + 1, 150, 20), "Press 'F' to pick '" + detectedItem.itemName + "'");
                GUI.color = Color.green;
                GUI.Label(new Rect(Screen.width / 2 - 75, Screen.height / 2 - 50, 150, 20), "Press 'F' to pick '" + detectedItem.itemName + "'");
            }
        }
    }
}

שלב 2: הגדר את הנגן ומערכת המלאי

נתחיל בהגדרת הנגן שלנו:

  • צור GameObject חדש וקרא לו "Player"
  • צור קפסולה חדשה (GameObject -> 3D Object -> Capsule) הסר את רכיב Capsule Collider ואז הזיז את הקפסולה בתוך האובייקט "Player" ולבסוף שנה את מיקומו ל- (0, 1, 0)
  • הזז את המצלמה הראשית בתוך האובייקט "Player" ושנה את מיקומו ל- (0, 1.64, 0)

  • צרף SC_CharacterController לאובייקט "Player" (הוא יוסיף אוטומטית רכיב נוסף בשם Character Controller, ישנה את הערך המרכזי שלו ל- (0, 1, 0))
  • הקצה את המצלמה הראשית למשתנה "Player Camera" ב-SC_CharacterController

עכשיו בואו נגדיר פריטי Pick Up - אלו יהיו Prefabs של הפריטים שניתן לבחור במשחק.

עבור הדרכה זו, אני אשתמש בצורות פשוטות (קוביה, צילינדר וכדור) אבל אתה יכול להוסיף דגמים שונים, אולי כמה חלקיקים וכו'.

  • צור GameObject חדש וקרא לו "SimpleItem"
  • צור קובייה חדשה (GameObject -> 3D Object -> Cube), קנה קנה מידה ל-(0.4, 0.4, 0.4) ואז הזז אותו לתוך "SimpleItem" GameObject
  • בחר "SimpleItem" והוסף רכיב Rigidbody וסקריפט SC_PickItem

תבחין שיש 2 משתנים ב-SC_PickItem:

שם הפריט - this should be a unique name.
תצוגה מקדימה של פריט - a Texture that will be displayed in the Inventory UI, preferably you should assign the image that represents the item.

במקרה שלי שם הפריט הוא "Cube" ותצוגה מקדימה של פריט היא ריבוע לבן:

חזור על אותם שלבים עבור 2 הפריטים האחרים.

עבור פריט צילינדר:

  • שכפל אובייקט "SimpleItem" ושם לו "SimpleItem 2"
  • הסר קוביית ילד וצור צילינדר חדש (GameObject -> 3D Object -> צילינדר). העבר אותו לתוך "SimpleItem 2" וקנה קנה מידה ל- (0.4, 0.4, 0.4).
  • שנה את שם הפריט ב-SC_PickItem ל-"Cylinder" ואת התצוגה המקדימה של הפריט לתמונה של גליל

עבור פריט Sphere:

  • שכפל אובייקט "SimpleItem" ושם לו "SimpleItem 3"
  • הסר את קוביית הילד וצור כדור חדש (GameObject -> 3D Object -> Sphere). העבר אותו לתוך "SimpleItem 3" וקנה קנה מידה ל- (0.4, 0.4, 0.4).
  • שנה את שם הפריט ב-SC_PickItem ל-"Sphere" ואת תצוגה מקדימה של פריט לתמונה של כדור

כעת שמור כל פריט ב-Prefab:

הפריטים מוכנים כעת.

השלב האחרון הוא להגדיר את מערכת המלאי:

  • צרף את SC_InventorySystem לאובייקט "Player"
  • הקצה משתנה של מרקם צלבה (תוכל להשתמש בתמונה למטה או לקבל טקסטורות צלבה באיכות גבוהה מ-כאן):

  • הקצה את SC_CharacterController למשתנה "Player Controller" ב-SC_InventorySystem
  • עבור "Available Items" הקצאת פריטים מוקדמים שנוצרו בעבר (הערה: אלה צריכים להיות מופעים מוקדמים מתצוגת הפרויקט ולא אובייקטים של סצנה):

מערכת המלאי מוכנה כעת, בואו נבדוק אותה:

Sharp Coder נגן וידאו

הכל עובד כמצופה!

מָקוֹר
InventorySystem.unitypackage159.83 KB