unity3d射箭模拟小游戏

该博客是继上次打飞碟小游戏的另一次尝试,且此次设计过程是在上次的经验上完全的由自己完成,因此尽管经历了更多的弯折和死路,但终究也获得了更多。
项目的代码已经上传至github,可以点击此处获取:
射箭小游戏
下面就来分享下这款小游戏的制作过程。


介绍

该小游戏中,玩家可以控制一根箭矢,通过鼠标左键调整箭矢指向,键盘F按下蓄力,松开发射,命中远处的箭靶则加分。

游戏画面

游戏尚未编写更加具体的规则和玩法,因为这只是一个学习类的项目。
那么这次游戏的看点:

  1. 模拟箭矢的飞行;
  2. 鼠标控制箭矢的指向;
  3. 键盘按下蓄力,松开发射;
  4. 场景内箭矢管理(并不太尽如人意)。
  5. 箭矢中靶响应。

每次介绍制作过程时,更多的还是讲解途中了解到的新知识,或是新认知。


项目总览

首先来看一下这个项目的结构及内容:

对象总览

  • 其中Base_1是一个空对象,具有刚体组件。作用是模拟子物体Cube(箭矢实体)的围绕一点旋转的情况。
  • Cube即箭矢的实际碰撞盒,但不添加刚体组件,该点会在后面说明。
  • Mass也是一个空对象,但位置相对于Cube更加偏向箭矢头部一点点,用于模拟重心偏离,即箭矢箭头朝下落地的情况。这个需要配合函数AddForceAtPosition来实现该效果。
  • Cylinder是一个圆柱状的箭靶,这里不使用系统默认加上的 胶囊碰撞器 ,因为只要试着去编辑它的碰撞盒形状,你就会发现前后柱面会多一个半球状的碰撞盒突起,这会使得箭矢提前触发碰撞效果。因此这里取消掉默认碰撞器,转而使用Mesh Collider( 网格碰撞器 ),该碰撞器可以很好地拟合物体表面,但需要花费更多的计算资源。不过对于此处的单一物件就没什么关系了。
  • Manager_1是一个空物体,用于挂载控制类脚本。在这些脚本上可以设置一些游戏的参数。
  • Canvas,画布对象,用于分数显示。内容和作用与先前的游戏相同,想要了解的可以到那篇博客里去查阅或是自行搜索相关内容。

下面是各项的具体参数:
Base_1:
Base_1
Cube:
箭矢碰撞盒
Mass_1:
Mass_1
其中子物体的坐标是相对于它的上一级父物体的相对坐标,而非世界坐标。
这里要是想获取相对坐标的话,使用transform.localPosition,世界坐标使用transform.position

注意

  • 关于 父子物体 设置。子物体的运动受父物体的运动影响,也就是说父物体移动的话,子物体也会跟随着移动,子物体移动的话,父物体并不会改变位置。因此相关的操作都需要相对于父物体来进行。另外对于 刚体组件 来说,
    1. 父物体单独持有刚体组件,子物体都会具有刚体属性,即会发生碰撞以及受物理力的影响。
    2. 子物体单独持有刚体,父物体并不具备刚体性质,也就是不会受力也不会发生碰撞。
    3. 父子都持有刚体组件,双方并不会进行相同的行为,而是根据各自的情况而运动,都具有刚体性质。

所以一般来说只需要父物体持有刚体组件就可以了。碰撞器的体积为所有具有碰撞器组件的子物体体积。在这里就是Cube子物体。

  • 关于 重心 的设置。常见的AddForce及相关函数所添加的力是不会出现力矩的。换句话来说就是不会出现让物体旋转的力。很明显箭矢之所以能够命中,就是因为它的箭头更沉导致重心偏前。在重力的影响下最后会箭头指向地面。
    为了达成这一点,需要用到函数AddForceAtPosition,在某个点施加物理力。这样只要力不是指向质心,就会出现力矩。可以用这个函数来模拟重力的作用,而这就需要一个偏移的质心。也就是上面的 Mass_1 ,它只负责传入它的位置信息,即transform组件。注意这里需要用到该空对象的 世界坐标 而非相对坐标。

箭矢(Arrow)

这里所谓的箭矢指的是即将发射的箭矢对象,在该程序中,落地,中靶的箭矢将会被销毁挂载在上面的脚本,并在一定时间后被回收。

这里需要额外说明一下,脚本也是游戏对象的一个组件,不仅可以通过this.gameObject来访问对象,更可以通过GetComponent<"脚本名">()来找到相应脚本。
Destroy(Arrow)只是销毁挂载在游戏对象上的脚本对象,Destroy(Arrow.gameObject)才是销毁挂载该脚本的游戏对象,当然同时脚本对象也被销毁了。
具体实例可以看一下ArrowController中的RecycleArrow函数。

ArrowController.RecycleArrow

public void RecycleArrow()
{
    if(arrow != null && arrow.transform.position.y < -5)
    {
        Destroy(arrow.gameObject);
        isOver = false;
        variable = true;
    }
    if (arrow != null && arrow.stopped)
    {
        used.Add(arrow.gameObject);
        Destroy(arrow);
        isOver = false;
        variable = true;
    }
    if(interval < 30f)
    {
        interval += Time.deltaTime;
    }
    else
    {
        for(int i = 0; i < used.Count; i++)
        {
            Destroy(used[i].gameObject);
        }
        used.Clear();
        interval = 0;
    }
}

箭矢跟随鼠标旋转

鼠标只能在屏幕上移动,坐标原点是屏幕左上角。因此程序中根据鼠标在整个屏幕上的相对位置来设置相应时刻箭矢的旋转角度。为了不让箭矢因为旋转角度过大而离开了摄像机视角,程序中对旋转的范围作出了限制。
具体参数需要根据摄像机和箭矢的摆放位置来调整。摄像机距离箭矢越近,能够旋转的范围就越窄。
这里是设置该操作需要在按下鼠标左键后才能进行。

Arrow.Update:

private float maxYRotation = 90;
private float minYRotation = 0;
private float maxXRotation = 50;
private float minXRotation = 0;

private void Update()
{
    if (Input.GetMouseButton(0))
    {
        float xPosPer = Input.mousePosition.x / Screen.width;
        float yPosPer = Input.mousePosition.y / Screen.height;
        float xAngle = -Mathf.Clamp(yPosPer * maxXRotation, minXRotation, maxXRotation) + 35;
        float yAngle = Mathf.Clamp(xPosPer * maxYRotation, minYRotation, maxYRotation) - 60;
        transform.eulerAngles = new Vector3(xAngle, yAngle, 0);
    }
}

箭矢中靶的事件响应

箭矢碰撞处理

为了更好地管理每支箭矢(当然这里并未实现什么),程序将碰撞事件OnCollisionEnter放置在了箭矢脚本上。这里需要关注的是,由于碰撞事件会导致双方弹开,而不会产生嵌入的效果,因此需要在箭矢发生碰撞之后为其增加一个 触发器属性 ,即:
GetComponentInChildren<BoxCollider>().isTrigger = true;
可以看到因为碰撞是发生在子物体Cube上的,父物体也不存在碰撞器组件,因此需要设置的是箭矢的子物体。
当然为了让箭矢停留在箭靶上,还需要清除它的速度以及作用在它上面的力。最简便的方法就是在让它的速度清零后直接销毁它的刚体组件(后面在施加力时会提前检测该物体是否为刚体)。

Arrow.OnCollisionEnter

GetComponentInChildren<BoxCollider>().isTrigger = true;
GetComponent<Rigidbody>().velocity = Vector3.zero;
Destroy(GetComponent<Rigidbody>());

中靶计分

使用碰撞事件(OnCollisionEnter)而不是触发器事件(OnTriggerEnter)的一大便利之处就是它可以简单地通过自带参数(Collision类型)来获取碰撞点的坐标,进而计算距离靶心的长度,并转换成此次得分。具体方法见代码:

Arrow.OnCollisionEnter

Vector3 pos = collision.contacts[0].point;
curScore = fullScore - (int)(TargetTf.position - pos).magnitude;

其中Vector3.magnitude用于将一个Vector3坐标转换成它的长度。

另外为了便于后面的处理,箭矢类中还有两个bool类型变量,分别指明箭矢是否发生碰撞( isStopped )是否命中箭靶( isAttacked )


箭矢控制类(ArrowController)

箭矢回收

为了实现箭矢中靶或者落地后的管理,这里增加了一个GameObject类型的List数据。只要前面所说的isStopped为真,回收函数就会将该箭矢对象放入到List中去,并销毁挂载在它身上的Arrow脚本组件。List中的游戏对象会在一个周期的末尾被统一销毁。

这里本来是打算设计成每支箭矢分开计算存在时间,但这会增加代码的复杂度(增加一个List或者将游戏对象和对应计时器封装起来),也不是这次设计的目的,因此只是简单地实现为统一销毁。

另外一旦箭矢掉出到地面以外(position.y < -5),该游戏对象会被立即销毁。

在上述两种情况下,都意味着玩家可以获得下一支箭矢了。这里增加了一个bool变量 variable 用于记录这条信息。

ArrowController.RecycleArrow函数已在上面给出。

箭矢创建

就上述的 variable 变量,initArrow函数在它为真的时候重新创建并初始化一个箭矢对象,并获取相关的参数。注意最后需要将variable变量设置为false。

这里涉及到了两个数据,即箭矢游戏对象的子物体 Cube 的transform和Cube的子物体 Mass_1 的transform。在后面添加驱动力和重力时需要用到。

ArrowController.initArrow

public void initArrow()
{
    if (variable)
    {
        arrow = Instantiate(_arrow);
        arrow.gameObject.SetActive(true);
        ChildTf = arrow.transform.GetChild(0).transform;
        ChildTf_2 = ChildTf.GetChild(0).transform;
        variable = false;
        isOver = false;
    }
}

箭矢运动

键盘按下蓄力

这里需要两个监听函数Input.GetKeyInput.GetKeyUp

  • GetKey函数监听键盘按下去这一 状态 ,只要在该帧键盘按下未松开,那么这个函数就会返回真。
  • GetKeyUp / GetKeyDown函数监听键盘按下或是抬起这一 动作 ,也就是说只会在这个动作发生的那一帧才会返回真。

GetMouseButton和GetButtonDown / GetButtonUp这对函数同样如此。

程序在这里同样设置了最小值和最大值。只要蓄力时间不够,就会默认使用最小值。超过蓄力时间也会默认使用最大值。

这里利用GetKeyUp这一短暂的一帧来设置相关参数。因为为物理施加力这一动作必须要在蓄力完成之后才可以进行。控制这一行为的是一个bool型变量 isOver ,它在键盘抬起这一帧内被设置成true,并在之后控制力的施加。
另外一个变量 ExPosition 是驱动箭矢前进的力的方向,是根据箭矢游戏对象上的两个子物体的坐标相减计算得出。
函数Nomalize将会把调用它的Vector3对象规格化为长度为1的值。

箭矢驱动

这里需要补充的是:驱动箭矢前进的力在初始大小的基础上设置了存在时间和递减效果,用于模拟弓弦对箭矢的作用。当然力应该不会是线性递减的,这里只是一个大概的模拟。
同时还为箭矢附加了一个阻力(drag)。

ArrowController.runArrow

public void RunArrow()
{
    if (arrow != null)
    {
        Rigidbody rb = arrow.GetComponent<Rigidbody>();
        if (rb)
        {
            rb.drag = 0.5f;
            if (!isOver)
            {
                if (Input.GetKey(KeyCode.F))
                {
                    if (Pressure < MaxPressure)
                    {
                        Pressure += Time.deltaTime * speed;
                    }
                    else
                    {
                        Pressure = MaxPressure;
                    }
                }
                else
                {
                    if (Pressure < MinPressure)
                    {
                        Pressure = MinPressure;
                    }
                }
                if (Input.GetKeyUp(KeyCode.F))
                {
                    clear = Pressure;
                    Pressure = 0;
                    Count = 0;
                    isOver = true;
                    ExPosition = ChildTf_2.position - ChildTf.position;
                    ExPosition.Normalize();
                }
            }
            else
            {
                if (Count < 0.5f)
                {
                    Count += Time.deltaTime;
                    rb.AddForce(ExPosition * clear * (1 - Count / 1f));
                }
                rb.AddForceAtPosition(Vector3.down * 9.8f, ChildTf_2.position);
            }
        }
    }
}

场景控制类(SceneController)

这一方面涉及到资源加载 SSDirector类 , 单例 Singleton类 的使用以及 分数显示 。使用方法和上次博客中谈及的类似。

SceneController

public class SceneController : MonoBehaviour
{
    ArrowController ac;
    SSDirector ssDirector;
    public Text Score;
    private int score;
    private int curScore;

    private void Awake()
    {
        ssDirector = SSDirector.getInstance();
        ssDirector.setFPS(30);
        ssDirector.sceneController = this;
        ssDirector.sceneController.LoadResources();
        score = 0;
    }

    private void LoadResources()
    {
        ac = Singleton<ArrowController>.instance;
        ac.initController();
    }

    private void Update()
    {
        ac.initArrow();
        ac.RecycleArrow();
        curScore = ac.getScore();
        if(curScore > 0)
        {
            score += curScore;
            setTextContent();
        }
    }

    private void FixedUpdate()
    {
        ac.RunArrow();
    }

    void setTextContent()
    {
        print("Attacked! this turn's score is: " + curScore);
        Score.text = "Score: " + score.ToString();
    }
}
  • 1
    点赞
  • 0
    评论
  • 17
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值