3D游戏设计第七次作业——Shoot Target(打靶)

展示链接:打靶子(ShootTarget)_网络游戏热门视频

源码链接:传送门

游戏玩法

玩家将操控十字弩在游戏场景中移动,如果移动到了指定场景,玩家可以通过点击鼠标左键蓄力射出弓箭,当弓箭射中靶子之后,玩家可以获得相应的分数。值得注意的是,每个指定位置可以使用的弓箭的数量是有限的,当箭被射完后,玩家将无法再射箭。按R键可以重置游戏。

游戏画面

游戏场景的构建

地形构建

在层级选项卡中点击鼠标右键,选中3D对象,找到并创建地形

选中创建的地形,在检查器中找到Terrain属性,将这个位置调整为Raise or Lower Terrain

然后使用笔刷在画面中点点画画,以创造不同的地形

然后在检查器中找到Terrain属性,将这个位置调整为Paint Texture,选择编辑地形层,然后导入图片材料

在绘制好的地形上的不同位置涂上不同的图片。

生成树

选中创建的地形,在检查器中找到Terrain属性,选择箭头所指的绘制树,再点击编辑树以导入树的预制体,然后在地形上画出预制体即可。

生成草

和生成树一样。

制作靶子

创建一个空对象,然后在空对象中创建五个圆柱体,5个圆柱体数据如下:

记得给每个圆柱体加上Mesh Collider的碰撞组件。

游戏代码实现

首先我们创建了一个随时间改变天空盒的脚本ChangeSky.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ChangeSky : MonoBehaviour
{
    public Material[] skyboxMaterials;
    int index;
    // Start is called before the first frame update
    void Start()
    {
        index = skyboxMaterials.Length - 1;
        StartCoroutine(ChangeSkyBox());
    }

    IEnumerator ChangeSkyBox()
    {
        while (true)
        {
            // 随机选择一个天空盒子材质
            index = (index + 1) % skyboxMaterials.Length;
            RenderSettings.skybox = skyboxMaterials[index];

            // 等待一段时间后再切换天空盒子
            yield return new WaitForSeconds(10);
        }
    }
}

生成靶子和射击区域TargetGenerator.cs

using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;

public class TargetGenerator : MonoBehaviour
{
    public GameObject targetPrefab; // 目标对象的预制体
    public GameObject shootAreaPrefab; // 设计区域预制体
    public int numberOfTargets = 6; // 要生成的目标数量
    public int ArrowCount = 6;
    public List<int> arrows = new List<int>();

    void Start()
    {
        GenerateTargets();
    }

    void GenerateTargets()
    {
        for (int i = 0; i < numberOfTargets; i++)
        {
            // 随机生成目标的位置
            Vector3 randomPosition;

            randomPosition = new Vector3((i + 1) * 10 + Random.Range(-2.5f, 2.5f) - 60, Random.Range(2f, 2.5f), Random.Range(-17f, 3f));

            // 使用预制体在随机位置生成目标
            GameObject target = Instantiate(targetPrefab, randomPosition, Quaternion.identity);

            GameObject shootArea = Instantiate(shootAreaPrefab, new Vector3(randomPosition.x, 0, randomPosition.z - 15f), Quaternion.identity);

            // 设置目标的名称
            target.name = "Target" + (i + 1);

            shootArea.name = "ShootArea" + (i + 1);

            if(i % 2 != 0)
            {
                // 添加 Animator 组件并设置动画控制器
                Animator targetAnimator = target.GetComponent<Animator>();
                if (targetAnimator != null)
                {
                    // 这里假设你有一个名为 "TargetController" 的动画控制器
                    if(i == 1)
                    {
                        targetAnimator.runtimeAnimatorController = Resources.Load<RuntimeAnimatorController>("Animation/Target");
                        shootArea.transform.position = new Vector3(0, 0, -14f);
                    }else if(i == 3)
                    {
                        targetAnimator.runtimeAnimatorController = Resources.Load<RuntimeAnimatorController>("Animation/Target1");
                        shootArea.transform.position = new Vector3(25f, 0, -7f);
                    }
                    else
                    {
                        targetAnimator.runtimeAnimatorController = Resources.Load<RuntimeAnimatorController>("Animation/Target2");
                        shootArea.transform.position = new Vector3(10f, 0, -16f);
                    }
                    
                }
            }
            arrows.Add(ArrowCount);
        }
    }

    // 调用这个函数来删除指定标签的游戏对象
    private void DeleteObjectsWithLabel(string label)
    {
        // 获取场景中所有的游戏对象
        GameObject[] allObjects = GameObject.FindObjectsOfType<GameObject>();

        // 遍历游戏对象数组
        foreach (GameObject obj in allObjects)
        {
            // 检查游戏对象是否有标签,并且标签匹配
            if (obj.CompareTag(label))
            {
                // 删除游戏对象
                Destroy(obj);
            }
        }
    }

    public void Reset()
    {
        arrows.Clear();
        DeleteObjectsWithLabel("Target");
        DeleteObjectsWithLabel("ShootArea");
        GenerateTargets();
    }

}

控制十字弩的运动,射击CrossbowMove.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CrossbowMove : MonoBehaviour
{
    Camera cam;
    CharacterController playerController;

    Vector3 direction;

    public float speed = 5;
    public float jumpPower = 3;
    public float gravity = 9.8f;
    public float mousespeed = 10f;

    public float minmouseY = -45f;
    public float maxmouseY = 45f;

    float RotationY = 0f;
    float RotationX = 0f;

    float time;

    bool canShoot = false;

    int pos = -1;

    // Start is called before the first frame update
    void Awake()
    {
        cam = Camera.main;
        cam.transform.parent = this.transform;
        cam.transform.localPosition = new Vector3(0, 1, 0);
        cam.transform.localRotation = Quaternion.Euler(45, 0, 0);
        playerController = this.GetComponent<CharacterController>();
        canShoot = false;
    }

    // Update is called once per frame
    void Update()
    {
        HandleRotationInput();

        // Handle ground movement and jumping
        if (IsGrounded())
        {
            HandleGroundMovement();
        }

        ApplyGravity();

        // Move the player
        playerController.Move(direction * Time.deltaTime * speed);

        // Shoot
        Shoot();
    }

    void HandleRotationInput()
    {
        float mouseX = Input.GetAxis("Mouse X") * mousespeed;
        float mouseY = -Input.GetAxis("Mouse Y") * mousespeed; // Inverted for more intuitive camera control

        RotationX += mouseX;
        RotationY = Mathf.Clamp(RotationY + mouseY, minmouseY, maxmouseY);

        // Apply rotation using Quaternion
        transform.rotation = Quaternion.Euler(RotationY, RotationX, 0);
        cam.transform.localRotation = Quaternion.Euler(RotationY / 4, 0, 0);
    }

    bool IsGrounded()
    {
        float raycastDistance = 5f; // 调整为适当的值
        float characterHeight = 1.1f; // 角色的高度,调整为适当的值

        // 发射射线向下检测地面
        if (Physics.Raycast(transform.position, Vector3.down, out RaycastHit hit, raycastDistance))
        {
            // 计算地面和角色的高度差
            float groundHeight = hit.point.y;
            float heightDifference = transform.position.y - groundHeight;

            // 如果高度差小于等于角色高度的一半,则认为角色在地面上
            return heightDifference <= characterHeight;
        }

        // 如果射线未击中地面,则认为角色不在地面上
        return false;
    }

    void HandleGroundMovement()
    {
        float horizontal = Input.GetAxis("Horizontal");
        float vertical = Input.GetAxis("Vertical");

        // Calculate movement direction based on rotation
        Vector3 forward = transform.forward;
        Vector3 right = transform.right;

        // Calculate movement input
        Vector3 moveDirection = horizontal * right + vertical * forward;
        moveDirection.Normalize();

        // Jumping
        if (Input.GetKeyDown(KeyCode.Space))
        {
            moveDirection.y = jumpPower;
        }

        // Set the direction
        direction = moveDirection;
    }

    void ApplyGravity()
    {
        // Apply gravity
        direction.y -= gravity * Time.deltaTime;
    }

    void Shoot()
    {
        TargetGenerator t = FindObjectOfType<TargetGenerator>();
        if (canShoot && t.arrows[pos] > 0)
        {
            View view = FindObjectOfType<View>();
            if (Input.GetMouseButtonDown(0))
            {
                time = 0;
            }
            else if (Input.GetMouseButton(0))
            {
                time += Time.deltaTime;
                float power = Mathf.Clamp01(GetComponent<Animator>().GetFloat("Power") + time / 100);
                GetComponent<Animator>().SetFloat("Power", power);
                view.UpdatePower(power);
            }
            else if (Input.GetMouseButtonUp(0))
            {
                GetComponent<Animator>().SetTrigger("Shoot");
                GetComponent<Animator>().SetFloat("Power", 0.5f);
                view.UpdatePower(0f);
                // Instantiate arrow with a slight offset in the Y-axis
                GameObject arrowPrefab = Resources.Load<GameObject>("Prefabs/Arrow");
                Vector3 arrowSpawnPosition = transform.position + new Vector3(0, 1.0f, 0); // Adjust the Y-axis offset
                GameObject arrow = Instantiate(arrowPrefab, arrowSpawnPosition, transform.rotation);
                GameObject subCamPrefab = Resources.Load<GameObject>("Prefabs/subCamera");
                Vector3 subCamSpawnPosition = arrow.transform.position + new Vector3(0, 0.2f, -0.5f);
                Quaternion rotation = Quaternion.Euler(0, 0, 0);
                GameObject subCam = Instantiate(subCamPrefab, subCamSpawnPosition, rotation);
                subCam.transform.SetParent(arrow.transform);

                // Add Rigidbody component to the arrow
                Rigidbody arrowRigidbody = arrow.GetComponent<Rigidbody>();

                // Set velocity to the arrow based on the character's forward direction and power
                float arrowSpeed = Mathf.Min(10.0f + time * 5, 20f); // Adjust the speed as needed
                arrowRigidbody.velocity = transform.forward * arrowSpeed;
                t.arrows[pos]--;
            }
        }
    }

    private void OnTriggerStay(Collider other)
    {
        if (other.CompareTag("ShootArea"))
        {
            // 获取碰撞的具体圆柱体名称
            string shootAreaName = other.name.Substring("ShootArea".Length);

            // 提取圆柱体名称中的数字
            int shootAreaNumber;
            if (int.TryParse(shootAreaName, out shootAreaNumber))
            {
                pos = shootAreaNumber - 1;
            }
            canShoot = true;
            View view = FindObjectOfType<View>();
            TargetGenerator t = FindObjectOfType<TargetGenerator>();
            view.UpdateShoot(canShoot);
            view.UpdateArrowCount(t.arrows[pos]);
        }
    }

    private void OnTriggerExit(Collider other)
    {
        if (other.CompareTag("ShootArea"))
        {
            pos = -1;
            canShoot = false;
            View view = FindObjectOfType<View>();
            view.UpdateShoot(canShoot);
        }
    }

}

对箭的控制ArrowBehavior.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Windows.WebCam;

public class ArrowBehavior : MonoBehaviour
{
    private bool isFixed = false; // 用于标记箭是否已经固定在胶囊体上

    private void OnCollisionStay(Collision collision)
    {
        if (!isFixed)
        {
            // 检测碰撞对象是否是圆柱体
            if (collision.collider.CompareTag("Target"))
            {
                // 获取碰撞的具体圆柱体名称
                string cylinderName = collision.collider.name;      

                // 提取圆柱体名称中的数字
                int cylinderNumber;
                if (int.TryParse(cylinderName, out cylinderNumber))
                {
                    // 在这里可以根据具体的圆柱体编号执行相应的逻辑
                    Debug.Log("Arrow hit capsule: " + cylinderNumber);

                    transform.SetParent(collision.transform.parent.transform);

                    // 固定箭在圆柱体上
                    FixArrowToCapsule(collision.contacts[0].point);
                    StartCoroutine(DisableSubCamAfterDelay(transform.GetChild(3).gameObject, 2.0f));

                    // 获取记分系统并调用加分方法
                    ScoreManager scoreManager = FindObjectOfType<ScoreManager>();
                    View view = FindObjectOfType<View>();
                    if (scoreManager != null)
                    {
                        string TargetName = collision.transform.parent.name.Substring("Target".Length);
                        int TargetNumber;
                        if (int.TryParse(TargetName, out TargetNumber))
                        {
                            if(TargetNumber %2 == 0)
                            {
                                scoreManager.IncreaseScore(cylinderNumber + TargetNumber);
                            }
                            else
                            {
                                scoreManager.IncreaseScore(cylinderNumber);
                            }
                            view.UpdateScore(scoreManager.score);
                            view.UpdateMaxScore(scoreManager.score);
                            view.AddHitTarget(collision.transform.parent.name, cylinderNumber);
                        }
                    }

                }
            }
            else if (collision.transform.name == "Ground")
            {
                Destroy(this.gameObject);
            }
        }
    }

    private void FixArrowToCapsule(Vector3 contactPoint)
    {
        // 固定标记设为 true
        isFixed = true;

        // 可以执行其他固定逻辑,例如禁用箭的 Rigidbody 组件等
        Rigidbody arrowRigidbody = GetComponent<Rigidbody>();
        if (arrowRigidbody != null)
        {
            arrowRigidbody.isKinematic = true; // 禁用 Rigidbody 的运动
        }

        // 将箭的位置调整到碰撞点
        transform.GetChild(2).position = contactPoint;

        transform.GetChild(1).gameObject.SetActive(false);

        transform.rotation = Quaternion.identity;
    }

    private IEnumerator DisableSubCamAfterDelay(GameObject subCam, float delay)
    {
        yield return new WaitForSeconds(delay);

        // 禁用subCam
        subCam.gameObject.SetActive(false);
    }

}

记分ScoreManager.cs

using UnityEngine;

public class ScoreManager : MonoBehaviour
{
    public int score;

    public ScoreManager()
    {
        score = 0;
    }

    public void IncreaseScore(int s)
    {
        // 箭射中目标,增加分数
        score += s;
    }

    public void Reset()
    {
        score = 0;
    }
}

界面View.cs

using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

public class View : MonoBehaviour
{
    private int score = 0;
    private float power = 0.0f;
    private float maxPower = 1.0f;
    private bool shoot = false;
    private int arrowCount = 6;
    private int maxScore = 0;

    // 记录击中箭靶的列表
    private List<string> hitTargets = new List<string>();
    private List<int> hitPoints = new List<int>();

    // 用于滚动的位置
    private Vector2 scrollPosition = Vector2.zero;

    private void OnGUI()
    {
        // 设置GUI样式
        GUIStyle style = new GUIStyle();
        style.fontSize = 24;  // 设置默认字体大小
        style.normal.textColor = Color.black;

        // 显示分数
        GUI.Label(new Rect(10, 10, 200, 30), "得分: " + score, style);

        // 显示最高分数
        GUI.Label(new Rect(120, 10, 200, 30), "最高分: " + maxScore, style);

        // 显示蓄力条
        DrawPowerBar();

        // 绘制红点在屏幕中央
        float dotSize = 10f;
        float dotPositionX = Screen.width / 2 - dotSize / 2;
        float dotPositionY = Screen.height / 2 - dotSize / 2;

        Texture2D redDotTexture = new Texture2D(1, 1);
        redDotTexture.SetPixel(0, 0, Color.red);
        redDotTexture.Apply();

        GUI.DrawTexture(new Rect(dotPositionX, dotPositionY, dotSize, dotSize), redDotTexture);

        // 重置游戏
        GUI.Label(new Rect(Screen.width - 210, Screen.height - 55, 200, 30), "按R键重置游戏", style);

        // 设置列表的位置和大小
        float listPositionX = 10f;
        float listPositionY = 60f;
        float listItemHeight = 20f;  // 设置列表项默认高度
        float listHeight = 8 * listItemHeight;  // 固定列表高度,超过这个高度将出现滚动条

        // 使用ScrollView实现滚动
        scrollPosition = GUI.BeginScrollView(new Rect(listPositionX, listPositionY, 200, listHeight), scrollPosition, new Rect(0, 0, 200, hitTargets.Count * listItemHeight));

        // 遍历击中箭靶的列表,并显示记录
        for (int i = 0; i < hitTargets.Count; i++)
        {
            // 设置记录的字体大小为16
            GUIStyle recordStyle = new GUIStyle(style);
            recordStyle.fontSize = 16;
            recordStyle.normal.textColor = Color.black;
            GUI.Label(new Rect(0, i * listItemHeight, 200, listItemHeight), "恭喜射中" + hitTargets[i] + "的" + hitPoints[i] + "环", recordStyle);
        }

        // 结束ScrollView
        GUI.EndScrollView();

        if (!shoot)
        {
            // 设置字体
            GUIStyle shootStyle = new GUIStyle(style);
            shootStyle.fontSize = 16;
            shootStyle.normal.textColor = Color.red;
            GUI.Label(new Rect(10, Screen.height - 30f, 90.0f, 30.0f), "您现在不能射击,请前往指定区域", shootStyle);
        }
        else
        {
            // 设置字体
            GUIStyle shootStyle = new GUIStyle(style);
            shootStyle.fontSize = 16;
            shootStyle.normal.textColor = Color.magenta;
            GUI.Label(new Rect(10, Screen.height - 30f, 90.0f, 30.0f), "您现在能射击了!!!", shootStyle);
            shootStyle.normal.textColor = Color.yellow;
            GUI.Label(new Rect(Screen.width / 2 - 45.0f, 30.0f, 90.0f, 30.0f), "此处还剩箭:" + arrowCount + "只", shootStyle);
        }
    }

    // 提供一个方法用于更新分数
    public void UpdateScore(int newScore)
    {
        score = newScore;
    }

    public void UpdateMaxScore(int newScore)
    {
        if(score > maxScore)
        {
            maxScore = score;
        }
    }

    public void UpdatePower(float newPower)
    {
        power = newPower;
    }

    public void UpdateShoot(bool newShoot)
    {
        shoot = newShoot;
    }

    public void UpdateArrowCount(int newArrowCount)
    {
        arrowCount = newArrowCount;
    }

    // 添加箭靶击中记录
    public void AddHitTarget(string targetName,int hitpos)
    {
        hitTargets.Add(targetName);
        hitPoints.Add(hitpos);
    }

    // 绘制蓄力条
    private void DrawPowerBar()
    {
        float barLength = 200f;
        float barHeight = 20f;
        float barPositionX = (Screen.width - barLength) - 10;
        float barPositionY = Screen.height - barHeight - 5;

        // 绘制蓄力条的背景
        GUI.Box(new Rect(barPositionX, barPositionY, barLength, barHeight), "Power");

        // 计算蓄力条的填充比例
        float fillRatio = power / maxPower;

        // 绘制蓄力条的填充部分
        GUI.Box(new Rect(barPositionX, barPositionY, barLength * fillRatio, barHeight), GUIContent.none);
    }

    public void Reset()
    {
        score = 0;
        power = 0;
        maxPower = 1.0f;
        shoot = false;
        arrowCount = 6;
        hitTargets.Clear();
        hitPoints.Clear();
        scrollPosition = Vector2.zero;
    }
}

重置游戏ResetManager.cs

using UnityEngine;

public class ResetManager : MonoBehaviour
{
    // 引用View、ScoreManager和TargetGenerator脚本
    private View view;
    private ScoreManager scoreManager;
    private TargetGenerator targetGenerator;

    private void Awake()
    {
        view = FindObjectOfType<View>();
        scoreManager = FindObjectOfType<ScoreManager>();
        targetGenerator = FindObjectOfType<TargetGenerator>();
    }

    void Update()
    {
        // 当按下R键时触发Reset函数
        if (Input.GetKeyDown(KeyCode.R))
        {
            // 调用View.cs中的Reset函数
            if (view != null)
            {
                view.Reset();
            }

            // 调用ScoreManager.cs中的Reset函数
            if (scoreManager != null)
            {
                scoreManager.Reset();
            }

            // 调用TargetGenerator.cs中的Reset函数
            if (targetGenerator != null)
            {
                targetGenerator.Reset();
            }
        }
    }
}

动画制作

CTRL+6唤出动画选项卡

选中要添加动画的游戏对象,编辑即可

  • 14
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值