找回密码
 立即注册
查看: 700|回复: 0

从零开始用Unity做一个海战游戏(上)

[复制链接]
发表于 2021-2-3 09:15 | 显示全部楼层 |阅读模式
本篇难度:★☆☆☆☆
前言

大家好。思索良久我决定鼓起勇气开一个稍微大点的新坑。
初衷是因为本人比较喜欢那种从无到有创造的乐趣,想做一个稍微完整点的小项目自娱自乐,也算是给自己一个小挑战。所以这篇文章亦可看做是一个简易的开发日志。
同时为了给喜欢游戏开发,有志于投身于此的萌新们一些帮助,尽量不使用复杂的组件或插件,用代码来实现需求。开发中遇到一些坑我也会尽力详细的说明。目的是让只要有一点C#编程基础的萌新也能跟着完整的做出来。
不过就算是天马行空的想法也得有个现实的参照,创世也得讲个基本法吧?所以我也有个参考,原型来自于以前很喜欢玩的一款网页小游戏《宇宙海贼王》:
游戏大体的玩法就是去扮演一个宇宙海贼,通过掠夺星球获得更强的装备直到挑战海贼王。
游戏中的装备系统非常丰富,多种流派之间的变化也让游戏的可玩性极佳。
游戏地址:http://www.u77.com/game/1790
最终目标就是在Unity上复刻一个类似的游戏出来,只不过题材从宇宙战改成了海战,也许还会加入一些自己觉得好玩的点子进去。
小船造起来

虽然想法很丰满,但现实还是得从零开始一点一点的搭出来,就先从一艘简单的小船开始。
为了对得起这个标题所以咱干脆连素材也不要了,自己动手做。
只不过这可有些难为没有美术细胞的我了,最初我想直接用Unity自带的模型拼一个出来,结果是惨不忍睹,差点我就想弃坑了。
最后仔细想了下,如果建模这一步要自己来还得去求助一些工具。
但是面对繁杂的建模软件顿时又生出一些无力感,这些软件都需要花大量的时间和精力去学习,对于只想简简单单做个小游戏的我而言颇有些本末倒置的感觉。
那么有没有一款软件既能做出还看得过去的模型,又简单易用呢?有!
向大家安利一款功能强大且免费开源的体素制作工具MagicaVoxel,这里不详细介绍软件的使用方式了,因为是真的简单易学,就算没有任何美术基础也能轻松上手。
下载地址:http://ephtracy.github.io/
总之我花了几个小时鼓捣了一通,总算拼出一艘船来。
出来吧,我的海军梦幻号!
好吧,只能说勉强能看,可能有些美术出身的同学已经忍不住要吐槽了,但不管怎么说总算比上面那个来的强。
在这里有个要注意的地方,在MagicaVoxel中的坐标系的轴与Unity有些不一样,对应起来是Y->Z ,Z->Y,X轴则是一样的。然后将文件导出为obj格式就可以拖到Unity里使用了。
二营长的意大利炮

可以看到特意在船上留了两个炮座,所以下一步就是撸出一门炮来。这比起捏一艘船来可就简单多了,只用注意一点,把炮身和炮台独立分开来,至于为什么,容我先卖个关子。
然后回到Unity这里导入模型,拼装在一起,如果你是真的分开来做炮塔和炮身,那在Unity里你要把两者拼得严丝合缝会极其蛋疼。
这里说一个小技巧,先就把两者一起画出来保存一份,复制两份在MagicaVoxel里分别扣去彼此。
然后在Unity中把炮身设为炮塔的子物体,把炮身的坐标置零就行了。
现在先不慌把炮摆到船上去,我们先把开炮的效果做出来。
首先炮得有开火的间隔,这对有一定基础的同学来说并不难。声明一个float变量作为间隔时间,在Update里减去每帧时间,一旦小于零就是可以发射了。这里换个看起来更简单的方法,
在炮身上新建一个脚本:
public class weapons : MonoBehaviour
{
    //开火间隔时间
    public float FireFrequency = 0.4f;
    //上一次开火时间
    private float PrevFireTime = float.MinValue;

    //如果上次开火的时间加上开火间隔时间小于当前游戏进行的时间,则炮的冷却好了
    private bool CanShoot
    {
        get
        {
            return PrevFireTime + FireFrequency < Time.time ? true : false;
        }
    }
    void Update()
    {
        KeyUpdate();
    }
    void Fire()
    {
        Debug.Log("开火");
    }
    void KeyUpdate()
    {
        if (Input.GetKey(KeyCode.Mouse0))
        {
            if (CanShoot)
            {
                Fire();
                PrevFireTime = Time.time;
            }
        }
    }
}
是不是有一种简明扼要的感觉?如果能开炮那就开炮吧。简单的使用了C#的属性让代码的可读性提高了不少。
然后现在回到之前卖的关子上来,炮身和炮管分离是为了做一个简单的开炮动画。
可能提到动画大多数人想到的都是动作酷炫人形动画,但明显不能用在这里。
Unity里提供了一个非常实用的功能:AnimationCurve,作用是编辑一条曲线用在任何你能使用的地方。我们需要的是用这条曲线模拟大炮开炮时退膛的动画。
先在脚本中新建一条动画曲线,然后就可以在脚本的面板上编辑它了。
    public AnimationCurve LerpCurve = new AnimationCurve();


编辑界面有几条预设的曲线,我们选取其中一条先缓后急的稍作修改。
这条曲线代表的是值随着时间变化的关系,我们需要开炮的时候炮身急速后退,然后缓慢回膛的效果,因此拖动右边的端点到(0.4,0),左边到(0,-0.4)左右,然后在脚本里获取炮身的模型,让其坐标的Z值随着这条曲线变化。
void PositionUpdate()
    {
        float t = Time.time - PrevFireTime;
        model.localPosition = Vector3.forward * LerpCurve.Evaluate(t);
    }
如图,大致就是我们所要的效果:
再添加一些开炮的粒子效果,让表现力提高一点
可能会在后面的文章中介绍如何做一个简易粒子特效,这里暂且跳过
炮塔旋转控制

现在可以把炮搬到船上去啦。
首先在船的父节点上新建两个子节点作为炮的锚点,调整子节点的坐标分别与船上炮座的底部中心点吻合,然后把炮塔设为锚点的子物体,坐标归零。
大家都见过真实的战舰炮塔是怎么运动的:
1.炮塔是有转速的。
2.炮管是可以沿着X轴方向抬升下降的。
3.炮塔的转动是有限制角度的。
我们先简化一下难度,暂时不考虑炮管在X轴转动的问题。所以现在要实现的功能就是在限制角度内恒定速度旋转的炮塔。
匀速旋转这个问题不难,限制每帧的角速度就行,限制旋转角度这个问题就稍微麻烦一些了。
我们先假设把炮塔限制在正前方60度内转动。
我一开始的想法是计算炮塔当前朝向角度,如果超过限制角的一半则停止转动。
想法是没错的,但是有一个问题。在Unity里判断当前的欧拉角会有角度正负的区别,比如一个物体沿Y轴选转1度和旋转-359度,最后的朝向是一样的,但是旋转方向不同。这会让代码里角度判定条件出错。
所以换了个思路来解决这个问题。
public class TurretsContorl : MonoBehaviour
{
    [Range(30, 330)]
    public float LimitAngle = 60f;

    public int RotateSpeed = 180;

    public Camera cam;

    void Update()
    {
        RotateUpdate();
    }

    void RotateUpdate()
    {
        Vector3 limit_dir = GetMouseDir_limit(LimitAngle);

        Quaternion rotate = Quaternion.RotateTowards(transform.rotation,
                                                     Quaternion.LookRotation(limit_dir, transform.up),
                                                    RotateSpeed * Time.deltaTime);
        transform.rotation = rotate;
    }

    //获取限制角度内的方向
    Vector3 GetMouseDir_limit(float limit_angle)
    {
        Ray ray = cam.ScreenPointToRay(Input.mousePosition);
        RaycastHit hit;

        if (Physics.Raycast(ray, out hit))
        {
            Vector3 hitpos = new Vector3(hit.point.x, transform.position.y, hit.point.z);
            Vector3 dir = (hitpos - transform.position).normalized;
            Debug.DrawLine(hitpos, transform.position, Color.blue, 0.5f);

            //如果鼠标指向的方向超过了限制角的一半,返回限制角与父节点Z方向的乘积
            if (Vector3.Angle(transform.parent.forward, dir) > limit_angle / 2)
            {
                bool in_my_right = Vector3.Cross(transform.parent.forward, dir).y > 0;
                Quaternion q = Quaternion.AngleAxis((in_my_right ? limit_angle : -limit_angle) / 2, transform.parent.up);
                Debug.DrawRay(transform.position, q * transform.parent.forward * 5, Color.red, 0.5f);
                return q * transform.parent.forward;
            }
            else
            {
                Debug.DrawRay(transform.position, (hitpos - transform.position).normalized * 5, Color.green, 0.5f);
                return (hitpos - transform.position).normalized;
            }
        }
        else
        {
            return transform.parent.forward;
        }
    }
}
直接在获取目标向量时就计算好,在旋转时直接按恒定速度转就可以了。
用Debug.DrawLine或Gizmos等可视化手段对验证自己思路的正确性非常有帮助
蓝线表示鼠标到炮塔的向量,黄线限制角内的返回的目标向量,红线为超过限制角的目标向量。可以看到大致符合我们的想法,但是还没有完全解决,当把限制角加到一个大于180度的角度时出现了新的问题。
恐怕没有哪艘战舰的炮会指着自家的指挥塔吧
辅助线告诉我们获取的目标向量没有问题,问题出在了 Quaternion.LookRotation()这个函数上。计算时返回的是向量的两个夹角中较小的那个角度。但在我们的需求中,这块不能转向的区域是“禁区”,所以得换一个方法来解决。
还是用图来帮助我们进行思考,假设现在的限制角是约300度:
由前面的结果可知当限制角在正前方180度(绿色区域)时没有问题,而无论是目标向量还是炮塔的当前朝向落在蓝色区域中时,我们就需要特殊处理一下了。
具体思路是当目标向量不在绿色区域时,先判断炮塔的当前朝向转向目标向量的方向。如果这个旋转方向朝向红色区域,再判断朝向与限制角边界的夹角和朝向与目标向量夹角之间的大小关系,可得知旋转的路径是否会经过“禁区”。一旦条件成立,取反当前的转轴与角度。换成代码表示如下:
    void RotateUpdate()
    {
        Vector3 aixs;
        float angle;

        Vector3 limit_dir = GetMouseDir_limit(LimitAngle);

        //目标向量与基准Z轴正方向的左右关系
        bool tar_is_right = Vector3.Cross(limit_dir, transform.parent.forward).y > 0;
        //炮塔当前朝向与目标向量之间的左右关系
        bool cur_is_right = Vector3.Cross(transform.forward, limit_dir).y > 0;
        //当前朝向与基准正方向的左右关系
        bool tra_is_right = Vector3.Cross(transform.forward, transform.parent.forward).y > 0;

        //如果目标向量在正180度之外,作特殊处理
        if (Vector3.Dot(limit_dir, transform.parent.forward) <= 0)
        {
            //当前朝向的边界向量
            Vector3 edge = Quaternion.AngleAxis(LimitAngle / 2, tra_is_right ? transform.parent.up : -transform.parent.up) * transform.parent.forward;
            //朝向与边界的夹角
            float edge_angle = Vector3.Angle(transform.forward, edge);
            //转向可能经过基准负方向且目标与边界的夹角小于朝向与边界的夹角,则角度和轴作取反处理
            if (((tar_is_right && cur_is_right) || (!tar_is_right && !cur_is_right)) && Vector3.Angle(limit_dir, edge) < edge_angle)
            {
                angle = 360 - Vector3.Angle(transform.forward, -limit_dir);
                aixs = Vector3.Cross(transform.forward, -limit_dir);
               
            }
            else
            {
                angle = Vector3.Angle(transform.forward, limit_dir);         
                aixs = Vector3.Cross(transform.forward, limit_dir);           
            }     
        }
        else
        {
            angle = Vector3.Angle(transform.forward, limit_dir);
            aixs = Vector3.Cross(transform.forward, limit_dir);
      
        }
        transform.Rotate(aixs, Mathf.Min(RotateSpeed * Time.deltaTime, angle));
    }
最终效果如我们所愿:
结束

在这期文章中我们把船的基本结构建立起来了,可以说开了个还算不错的头。
可能文章在解释遇到问题的时候稍显拖沓,但我始终觉得在开发时遇到和解决问题的过程才是最有价值的,因此使用这种有点记流水账的方式说明问题。同时限于自身的水平,难免会有疏漏和不足,也欢迎大家指正。
感谢观看到此,下期再见。(如果没有太监掉的话...)
本期工程地址:https://github.com/tank1018702/unity-004




有想系统学习游戏开发的童鞋,欢迎来http://levelpp.com/莅临指导。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-12-24 00:00 , Processed in 0.090917 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表