找回密码
 注册帐号

扫一扫,访问微社区

士郎 用Unity开发一款塔防游戏(一):攻击方设计

27
回复
1942
查看
[ 复制链接 ]
排名
1
昨日变化

8057

主题

8615

帖子

3万

积分

Rank: 16

UID
1231
好友
186
蛮牛币
12224
威望
30
注册时间
2013-7-29
在线时间
4126 小时
最后登录
2019-8-22

活力之星原创精华达人突出贡献奖财富之证游戏蛮牛QQ群会员蛮牛妹VIP

2019-6-12 16:12:55 显示全部楼层 阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

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

x
塔防游戏相信大家并不陌生,几个主要元素如下:

1、敌方士兵

2、我方防御塔

3、我方主城

emmmmmmm好像就没了。


人越狠,话越不多。不多说,接下来我们一步步把这几个功能做完。

素材准备:

网上随便找一些资源就行,不一定要和我一样。这里再次强调:

网上获取的资源一定不能用作商业用途!!!!!!

就本工程而言,资源有一下几种:

敌人2个,分别拥有移动,攻击,待机,死亡四种动画

1.jpg

防御塔3个,拥有待机,攻击两种动画

1.jpg
人形防御塔可还行

主城1个,主地形1组(内含各种杂草乱石)

1.jpg

敌人地形(敌人能用来走的路)1种,防御塔地形(防御塔能放置的地方)1种,箭矢1个


场景搭建:

先从简单的功能做起:让敌人从生成点走到主城,看见主城就攻击。

搭建一个简单场景:

1.jpg
为了检测敌人寻路,最好是能转弯的道路

敌人和主城有一个都有血量的属性,都会被攻击,这里为它们做能显示在头上的血条。

以主城为例,在主城的子节点层创建一个Sprite做黄血条,设为黄色,取名“BloodStrip”,调整好大小:

1.jpg

然后在BloodStrip的子节点层创建一个空物体,取名“Hp”,在Hp的子节点层再创建一个Sprite做红血条,名字“Red”,设为红色,大小和黄血条一样,把黄血色覆盖:

1.jpg

接下来就移动红血条位置,让它左边边缘与父物体Hp的Y轴重合:

1.jpg

然后再将Hp往右移动,让Y轴与黄血条左边缘重合(红血条刚好覆盖黄血条):

1.jpg

这样我们只需要设置H的X轴大小,就可以控制红血条长度了


***这里请初学者注意,如果你选取的红血条图片资源不是纯色的、是有其他花纹的,则不能用这个方法。原因很简单,这种方法会把花纹拉长或压扁。大家可以下来想一下:这种情况下应该怎样来设置?

后面在代码中只需要将当前血量与总血量的比值赋给Hp的X轴,就可以将血量信息显示在界面上了。敌人血条做法一样。

做好后让BloodStrip处于禁用状态,受伤后才显示(这是游戏UI显示的一个约定俗成的规则)。

代码编写:

为主城与敌人创建一个基类脚本Character:

[AppleScript] 纯文本查看 复制代码
public class Character : MonoBehaviour
{
    public float totalHp = 100; //总血量
    float surHp; //剩余血量
    protected Transform hpObj; //黄血条
    protected Transform redHp; //血条红条
    protected Transform mainCamera; //主摄像机

    public virtual void Init() //初始化
    {
        surHp = totalHp;
        hpObj = transform.Find("BloodStrip");
        redHp = hpObj.Find("Hp");
        mainCamera = GameObject.Find("Main Camera").transform;
    }
    public void Damage(float damage) //受伤方法,参数为受到的伤害值
    {
        if (surHp > damage) //当前血量大于受伤血量,正常扣血
        {
            surHp -= damage;
            //受伤后开始显示血条
            if (surHp < totalHp)
                hpObj.gameObject.SetActive(true);
            Vector3 hpScale = redHp.localScale;
            hpScale.x = surHp / totalHp;
            redHp.localScale = hpScale;
        }
        else //当前血量不够,调用死亡方法          
            Death();
    }
    public virtual void Death() //死亡方法
    {
        surHp = 0;
        hpObj.gameObject.SetActive(false); //血条不再显示
    }
}


创建主调脚本:用于游戏初始化和记录游戏死亡,挂在一个场景物体上:


[AppleScript] 纯文本查看 复制代码
public class GameMain : MonoBehaviour
{
    public static GameMain instance;
    public bool gameOver;
    void Start()
    {
        InitGame();
    }
    //初始化游戏
    void InitGame()
    {
        instance = this; //单例
        gameOver = false;
    }
}


创建主城脚本,继承自Character脚本:
[AppleScript] 纯文本查看 复制代码
public class MainCity : Character
{
    void Start()
    {
        Init();
    }
    private void Update()
    {
        hpObj.rotation = mainCamera.rotation; //血条始终面向镜头
    }
    public override void Death() //重新死亡方法
    {
        base.Death();
        GameMain.instance.gameOver = true; //游戏结束
    }
}


敌人的脚本也继承自Charater,除了受伤和死亡之外还能攻击与移动:
[AppleScript] 纯文本查看 复制代码
public class Enemy : Character
{
    Animator anim;
    public float damage; //伤害
    public float speed; //移动速度
    MainCity target; //主城
    public override void Init()
    {
        base.Init();
        anim = GetComponent<Animator>();
    }
    private void Update()
    {
        hpObj.rotation = mainCamera.rotation; //血条始终面向镜头
    }
    //前进方法
    private void EnemyForward()
    {
    }
    //攻击方法(放在攻击动画事件中)
    private void EnemyAttack()
    {
        if (target != null)
            target.Damage(damage);
    }
    //死亡方法
    public override void Death()
    {
        base.Death();
        anim.Play("death");
    }
    //尸体消失
    private void DestroySelf()
    {
        Destroy(gameObject);
    }
}


重点在移动方法上。因为敌人的移动带有寻路功能,这里没有采取Unity自带的NavMeshAgent,而是用脚本来实现,主要思路仿照盲人的行进方式,利用射线充当导盲棍,发现前方道路中断再从两边找新的行进路线:

1.jpg
拐杖就是射线

要利用好这个思路,场景中道路的搭建也有一定要求,道路都要挂上MeshCollider组件,方便射线检测。

1.jpg
所有道路的Z轴指向路线前进方向

道路的物体层设置为“Way”,主城也挂上碰撞器,物体层设为“City”。

1.png

在敌人模型身上创建一个空物体为眼睛,取名为“Eye”,主要作用是从此为射线起始点,位置合适即可,注意,因为所有敌人都用的相同脚本,所以所有敌人的眼睛高度距离地面相同:

1.jpg
正面看这些模型真特么惊悚

当然每个敌人也请挂上碰撞器和刚体以及Animator组件:

1.jpg

创建一个敌人状态机:
[AppleScript] 纯文本查看 复制代码
public enum EnemyState //状态机
{
    forward,
    attack,
    death
}


重写初始化方法:
[AppleScript] 纯文本查看 复制代码
 Animator anim;
    Rigidbody rigid;
    public EnemyState state;
    Transform eye; //眼睛:用于观测道路和攻击目标
    List<Collider> ways; //记录走过的路(不走回头路)
    //重新初始化方法
    public override void Init() 
    {
        base.Init();
    
        anim = GetComponent<Animator>();
        rigid = GetComponent<Rigidbody>();
        gameObject.layer = LayerMask.NameToLayer("Enemy"); //敌人层设置为"Enemy"
        state = EnemyState.forward;
        eye = transform.Find("Eye");
        ways = new List<Collider>();
    }


编写移动方法,并在Update中调用:
[AppleScript] 纯文本查看 复制代码
private void Update()
    {
        hpObj.rotation = mainCamera.rotation; //血条始终面向镜头
        if (GameMain.instance.gameOver) //游戏结束播放待机动画
            anim.Play("idle");
        else if (state == EnemyState.forward)
            EnemyForward();
    }
    public int view; //视野
    Quaternion wayDir; //前进方向
    MainCity target; //主城
    Transform way; //正在走的路
    public float speed; 
    //前进方法
    private void EnemyForward()
    {
        RaycastHit hit;
        //看见攻击目标则攻击
        if (Physics.Raycast(eye.position, transform.forward, out hit, view, LayerMask.GetMask("City")))
        {
            state = EnemyState.attack;
            anim.Play("attack");
            target = hit.collider.GetComponent<MainCity>();
        }

        //斜下方30°打射线检测前方道路
        if (Physics.Raycast(eye.position, Quaternion.AngleAxis(30, transform.right)
            * transform.forward, out hit, 50, LayerMask.GetMask("Way")))
        {
            Debug.DrawLine(eye.position, hit.point, Color.blue);
            //发现未走过的道路,获取该道路,朝向该路通往的方向
            if (!ways.Contains(hit.collider))
            {
                ways.Add(hit.collider);
                way = hit.transform;
                wayDir = Quaternion.LookRotation(way.forward);
            }
        }
        else //前方没路了发射球形射线检测周围是否有路
        {
            Collider[] colliders = Physics.OverlapSphere(transform.position, 8, LayerMask.GetMask("Way"));
            for (int i = 0; i < colliders.Length; i++)
            {
                //发现未走过的道路,获取该道路,朝向该路通往的方向
                if (!ways.Contains(colliders[i]))
                {
                    way = colliders[i].transform;
                    wayDir = Quaternion.LookRotation(way.forward);
                    break;
                }
            }
        }
        //获取与脚下道路x轴上偏差值,好让自身走在路中间
        float offset = 0;
        if (way != null)
        {
            Vector3 distance = transform.position - way.position;
            offset = Vector3.Dot(distance, way.right.normalized);
        }
        //面向该路指向的方向前进
        transform.rotation = Quaternion.RotateTowards(transform.rotation, wayDir, speed * 20 * Time.deltaTime);
        transform.Translate(-offset * Time.deltaTime, 0, speed * Time.deltaTime);
    }


暂时把初始化方法放在Start中调用(后面我们会在创建的时候初始化),然后设置好血量、视野、速度、伤害,主城也设置好血量:

1.jpg

先来看下寻路运行效果:


蓝线检测前方道路,红圈检测周围道路

寻路没有问题了,将攻击动画设为循环播放,然后将攻击方法放入攻击动画事件中,敌人看到主城就会自动攻击了:

敌人主要功能就已经完成。现在我们来做敌人生成器。

塔防游戏的敌人生成方式一般都是比较有规律的,比如先生成一组a敌人,跟着生成一组b敌人,每组敌人的生成间隔也恒定(当然,读者也可以自己尝试更丰富的出兵方法,比如让“某些特定敌人的血量减到某个阈值”作为触发条件等等):

1.jpg

为了生成方便,我们来做一个定时器,可以重复并规律地调用一个生成敌人方法:
[AppleScript] 纯文本查看 复制代码
public class Util : MonoBehaviour
{
    private static Util _Instance = null;
    public static Util Instance //单例模式,依附GameObject
    {
        get
        {
            if (_Instance == null)
            {
                GameObject obj = new GameObject("Util");
                _Instance = obj.AddComponent<Util>();
            }
            return _Instance;
        }
    }
    public class TimeTask //定时事件类
    {
        public Action callback; //回调函数
        public float delayTime; //延迟长度
        public float destTime; //延迟后的目标时间
        public int count; //重复次数
    }                
    List<TimeTask> timeTaskList = new List<TimeTask>(); //保存所有的定时事件   
    //增加定时回调的方法 
    public void AddTimeTask(Action _callback, float _delayTime, int _count = 1)     
    {
        timeTaskList.Add(new TimeTask()
        {
            callback = _callback,
            delayTime = _delayTime,
            destTime = Time.realtimeSinceStartup + _delayTime,
            count = _count
        });
    }
    private void Update()
    {
        for (int i = 0; i < timeTaskList.Count; i++) //实时监测所有定时事件
        {
            TimeTask task = timeTaskList[i];
            if (Time.realtimeSinceStartup >= task.destTime) //时间到了,则执行
            {
                task.callback?.Invoke(); 
                if (task.count == 1) //当次数为1,执行完移除该定时事件
                    timeTaskList.RemoveAt(i);
                else if (task.count > 1) //当次数大于1,执行完次数减1
                    task.count--;
                task.destTime += task.delayTime; //执行完一次后,重新定出下次执行时间
            }
        }
    }
}


把所有敌人放入一个路径中,创建一个空物体做敌人生成器,放在敌人生成点,创建脚本挂上去:
[AppleScript] 纯文本查看 复制代码
public class EnemySystem : MonoBehaviour
{
    //根据名称保存所有敌人
Dictionary<string, Enemy> enemyDict = new Dictionary<string, Enemy>();
//初始化,放在主调脚本GameMain中执行
    public void Init()
    {
        //保存所有种类敌人,可以根据名字获取
        Enemy[] enemys = Resources.LoadAll<Enemy>("Prefab/Chara/EnemyChara");
        for (int i = 0; i < enemys.Length; i++)
        {
            if (!enemyDict.ContainsKey(enemys[i].name))
                enemyDict.Add(enemys[i].name, enemys[i]);
        }
    }
    //生成敌人,参数中设置敌人种类,生成间隔,生成数量(默认为1)
    public void CreateEnemy(string name, float delay, int count = 1)
    {
        if (GameMain.instance.gameOver == false)
            //使用定时器,生成敌人
            Util.Instance.AddTimeTask(() => Instantiate(
            enemyDict[name], transform.position, transform.rotation).Init(),
            delay, count);
}
    //点击按钮生成敌人(挂在按钮事件中)
    public void ClickButtonDispatchTroops()
    {
        //每秒生成一个敌人,生成5次,第一次生成在1秒后执行
        CreateEnemy("Zombie1", 1, 5); 
        //没0.5秒生成一个敌人,生成10次,第一次生成在5.5秒后执行
        Util.Instance.AddTimeTask(() => CreateEnemy("Zombie2", 0.5f, 10), 5);
    }
}


做到这一步就可以像演示视频中那样点击按钮出兵了。

放上工程链接:

https://pan.baidu.com/s/1T2nZ_FrIk9DaTvem-YH8nQ提取码:n61s

知乎@四五二十
回复

使用道具 举报

0

主题

11

帖子

49

积分

Rank: 1

UID
299963
好友
0
蛮牛币
62
威望
0
注册时间
2018-10-14
在线时间
38 小时
最后登录
2019-8-22
2019-6-12 21:20:04 显示全部楼层
非常感谢楼主,刚好缺这方面的姿势
回复 支持 反对

使用道具 举报

7日久生情
2121/5000
排名
4092
昨日变化

0

主题

1396

帖子

2121

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
254705
好友
1
蛮牛币
1921
威望
0
注册时间
2017-11-16
在线时间
363 小时
最后登录
2019-8-22
2019-6-13 08:23:17 显示全部楼层
66666666666666666666666666666666
回复 支持 反对

使用道具 举报

4四处流浪
346/500
排名
8824
昨日变化

0

主题

39

帖子

346

积分

Rank: 4

UID
307305
好友
0
蛮牛币
685
威望
0
注册时间
2018-12-6
在线时间
173 小时
最后登录
2019-8-22
2019-6-13 08:38:06 显示全部楼层
感谢分享
回复

使用道具 举报

7日久生情
1839/5000
排名
1395
昨日变化

13

主题

262

帖子

1839

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
119664
好友
3
蛮牛币
8196
威望
0
注册时间
2015-8-25
在线时间
684 小时
最后登录
2019-8-21
QQ
2019-6-13 09:01:41 显示全部楼层
学习学习~~~~~~~~~~~~~~~
回复

使用道具 举报

5熟悉之中
616/1000
排名
16955
昨日变化

1

主题

440

帖子

616

积分

Rank: 5Rank: 5

UID
175005
好友
0
蛮牛币
736
威望
0
注册时间
2016-10-12
在线时间
135 小时
最后登录
2019-8-22
2019-6-13 09:11:50 显示全部楼层
不错
回复

使用道具 举报

4四处流浪
468/500
排名
7196
昨日变化

0

主题

137

帖子

468

积分

Rank: 4

UID
267358
好友
0
蛮牛币
651
威望
0
注册时间
2018-2-1
在线时间
151 小时
最后登录
2019-8-21
2019-6-13 09:53:00 显示全部楼层
学习大佬操作
回复

使用道具 举报

4四处流浪
385/500
排名
16594
昨日变化

1

主题

133

帖子

385

积分

Rank: 4

UID
281418
好友
0
蛮牛币
564
威望
0
注册时间
2018-5-16
在线时间
209 小时
最后登录
2019-8-20
2019-6-13 10:17:34 显示全部楼层
很有用        
回复 支持 反对

使用道具 举报

3偶尔光临
207/300

0

主题

91

帖子

207

积分

Rank: 3Rank: 3Rank: 3

UID
318682
好友
0
蛮牛币
325
威望
0
注册时间
2019-4-4
在线时间
116 小时
最后登录
2019-8-19
2019-6-13 10:22:15 显示全部楼层
感谢分享
回复

使用道具 举报

排名
64936
昨日变化
1

0

主题

7

帖子

16

积分

Rank: 1

UID
180527
好友
1
蛮牛币
37
威望
0
注册时间
2016-11-16
在线时间
7 小时
最后登录
2019-7-2
2019-6-13 10:49:00 显示全部楼层
6666666666666666
回复 支持 反对

使用道具 举报

2初来乍到
101/150
排名
13719
昨日变化

0

主题

8

帖子

101

积分

Rank: 2Rank: 2

UID
180322
好友
0
蛮牛币
304
威望
0
注册时间
2016-11-3
在线时间
31 小时
最后登录
2019-8-18
2019-6-13 11:53:52 显示全部楼层
感谢分享啊!!!
回复

使用道具 举报

5熟悉之中
771/1000
排名
10706
昨日变化

0

主题

512

帖子

771

积分

Rank: 5Rank: 5

UID
301976
好友
1
蛮牛币
1151
威望
0
注册时间
2018-10-31
在线时间
161 小时
最后登录
2019-8-22
2019-6-13 13:48:39 显示全部楼层
感谢分享...
回复

使用道具 举报

排名
64936
昨日变化
1

0

主题

11

帖子

22

积分

Rank: 1

UID
271058
好友
0
蛮牛币
24
威望
0
注册时间
2018-3-7
在线时间
9 小时
最后登录
2019-6-17
2019-6-13 13:56:57 显示全部楼层
366666666666666
回复 支持 反对

使用道具 举报

2初来乍到
113/150
排名
13985
昨日变化

0

主题

16

帖子

113

积分

Rank: 2Rank: 2

UID
306015
好友
0
蛮牛币
105
威望
0
注册时间
2018-11-26
在线时间
37 小时
最后登录
2019-8-9
2019-6-13 15:29:07 显示全部楼层
感谢分享
回复

使用道具 举报

7日久生情
1511/5000
排名
1338
昨日变化

8

主题

149

帖子

1511

积分

Rank: 7Rank: 7Rank: 7Rank: 7

UID
215462
好友
1
蛮牛币
3766
威望
0
注册时间
2017-3-30
在线时间
442 小时
最后登录
2019-8-22
2019-6-13 17:12:23 显示全部楼层
跟大佬学习操作
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册帐号

本版积分规则