2017-08-07 08:52:59 a6958858 阅读数 10789
  • Unity 值得看的500+ 技术内容列表

    Unity3D是由Unity Technologies开发的一个让玩家轻松创建诸如三维视频游戏、建筑可视化、实时三维动画等类型互动内容的多平台的综合型游戏开发工具,是一个全面整合的专业游戏引擎。

游戏效果图:


本文参照siki学院的塔防游戏教程写的。http://www.sikiedu.com,搜索塔防游戏登入即可观看。

游戏流程:

1.首先创建cube调整其大小(以便计算),然后将其设成预设体(以便之后统一操作),利用ctrl+D复制弄出地形,在Hierarchy视图上创建一个map文件夹保存其文件。然后在地形内删除cube,创出自己想要的敌人行走路径。

2.在每个转折处设置一个Gameobject文件夹,然后在Transform上放修改一种颜色(以便区分),

将其命名为Waypoint并设置成预设体,创建一个Waypoint文件夹保存所有的Waypoint。在预设体上加上脚本Waypoint:

public class Waypoint : MonoBehaviour {
    public static Transform[] p;//定义数组
    private void Awake()//在脚本执行时执行
    {
        //获取每个数组的参数,也就是节点位置
        p = new Transform[transform.childCount];
        for (int i = 0; i < p.Length; i++) {
            p[i] = transform.GetChild(i);
        }
    }
}

在Scence场景上新建一个Sphere(当成敌人)然后将其放在起点,加上脚本Enemy:

public class Enemy : MonoBehaviour {

    public float speed = 10;//设置敌人的速度
    private Transform[] p;//定义数组

void Start () {
        p = Waypoint.p;//调用Waypoint脚本获取节点的位置信息
    }

void Update () {
        Move();//每一帧执行方法
}

void Move() {
        transform.Translate((p[index].position-transform.position).normalized*Time.deltaTime*speed);//移动,节点到当前位置的向量差的单位差*完成上一帧的时间*速度

        if (Vector3.Distance(p[index].position, transform.position) < 0.2f)//三维坐标,距离(节点,当前位置)小于0.2f的时候执行
        {
            index++;//增加索引,也就获取到下个节点坐标
            if (index > p.Length - 1)//如果大于最后一个节点时执行
            {
                Destroy(this.gameObject);//销毁物体
            }
        }
    }

}

这样就能让物体按照指定的坐标移动起来。

  3. 控制游戏的视野(设置摄像机),首先把摄像机调整到一个合适的状态(向上移动一定的位置,x轴旋转45°),之后来用脚本控制摄像机的移动,添加脚本Move():

public class move : MonoBehaviour {
// Use this for initialization
void Start () {

}

// Update is called once per frame
void Update () {
        float a = Input.GetAxis("Horizontal");//当按下的左右方向(a,d 方向键的左右 4,6)时获取到一个-1到1的值
        float b = Input.GetAxis("Vertical");//当按下的上下方向(w,s 方向键的上下 8,2)时获取到一个-1到1的值
        float m = Input.GetAxis("Mouse ScrollWheel")*-8;//鼠标滚轮缩放
        transform.Translate(new Vector3(a,m,b)*Time.deltaTime*50,Space.World);//转为世界坐标移动
}
}

4.创建一个Gameobject命名为fuhuaqi代表敌人孵化器(用来控制敌人的生成),首先写一个封装类来保存每一波敌人的类型,总数,速度,封装类命名为Bo:

[System.Serializable]//可序列化,就是能被其他代码引用的意思
public class Bo {
    public GameObject e;//敌人的类型
    public int count;//敌人的总数
    public float rate;//每个敌人的间隔
}

之后再fuhuaqi上写上控制敌人生成的脚本Enemyfuhuaqi:

public class Enemyfuhuaqi : MonoBehaviour {

    public Bo[] b;//封装类
    public Transform start;//定义开始位置
    public float jiange = 3;//每波的间隔

    private Coroutine coroutine;//定义协同,方便控制协同的启动和停止

    void Start () {
       coroutine= StartCoroutine(Move());//启动协程
    }

    public void Stop()
    {
        StopCoroutine(coroutine);//停止协程
    }

//协程
    IEnumerator Move() {
        foreach (Bo b in b)
        {
            for (int i = 0; i < b.count; i++)//循环生成
            {
                GameObject.Instantiate(b.e,start.position,Quaternion.identity);//创建(生成种类,生成位置,不旋转)
                if (i != b.count - 1)
                {
                    yield return new WaitForSeconds(b.rate);//协程中的等待(同样敌人的间隔)
                }
            }    
           yield return new WaitForSeconds(jiange);//完成一波后等待间隔的时间
    }

}

写好脚本后记得给相对应的东西赋值!

5.右键点击Assets->import package->custom package选择载好的材料包,将建筑模型拖入Scence然后在Transform的右侧的齿轮Reset,调整位子(注:选中模型里面所有子物体进行调整,因为之后要按照模型定位与地图方块的地位相同,不让其陷入地板),给对应的位置上材质。做好后设置成Prefab。

6.开始设置UI界面,首先创建一个Canvas(画布),点击Scence上的2D按钮界面会变成2D界面比较容易操作,在画布里在创建一个Gameobject(名称:Switch)在其下创建3个Canvas存放炮塔的图标在其下再创建image(名:background)和text(用来存放炮塔的图标图片和价格)在其background下创建image(名:Checkmark 用来表示被选中状态)将其中Soure image 属性改成Knob然后改变颜色改变透明度

给Switch添加属性Toggle group将每个炮塔添加属性Toggle并is on属性都取消,将Checkmark拖入Graphic,将Group都设置成Switch(表示在一个组里面)

7.在主Canvas下创建Text(名:money)用来显示当前的钱,设置一段动画让钱不够时会闪烁,选择菜单栏上的windows->animation->create->选择保存到Animations文件夹(文件夹自己定义),点击红点开始录制,然后按照喜好变化就好。双击动画进入编辑界面:


再fuhuaqi上添加脚本buildguanliqi,将定义好的文本和动画赋值上去

8.创建炮塔,首先选中右侧建造的炮塔,检查mapcube上是否为空,并且鼠标点击mapcube,即可建造。加上粒子效果,建造后money减少。

9.创建子弹,在头部定义子弹的位置实例化子弹,然后给炮塔加一个Shpere collider用来检测进入的敌人并用数组存放,并且默认攻击第一个,超出范围或者敌人死亡移除该物体在数组中的位置

10.升级炮塔,检测mapcube上是否有物体存在,物体是否升级过,钱是否够,满足条件这摧毁先前物体,新建升级物体并播放特效。

11.敌人全部死亡,显示胜利,到达终点则显示失败;

2015-12-15 11:08:56 fenrir_sun 阅读数 2775
  • Unity 值得看的500+ 技术内容列表

    Unity3D是由Unity Technologies开发的一个让玩家轻松创建诸如三维视频游戏、建筑可视化、实时三维动画等类型互动内容的多平台的综合型游戏开发工具,是一个全面整合的专业游戏引擎。

在一些射击,潜入,动作类游戏中,创关的时候总是会有敌兵守卫在某处,或者在一定范围内巡逻。这时候AI的逻辑就需要一个算法来判断何时攻击玩家或者何时发现玩家。一般的ARPG也许对此要求不高,只要玩家进入敌人的一定范围内(一个以敌人为中心的碰撞框)就开始攻击玩家。潜入类游戏还需要按照玩家发出的声音来搜索玩家。

判断士兵是否听到声音需要符合两个条件,一个是有发声源,一个是发生源到士兵的距离在士兵的听觉范围内。这里的距离不是直线距离,而是路径距离。路径距离可以利用Unity的NavMeshAgent来判断,前提是在场景中预先烘培了导航网格。

这里写图片描述

这里使用nav.CalculatePath()计算挂载脚本的gameObject和targetPosition之间的路径,然后再通过路径的corners来获取路径上的转折点,把每个点连起来就是声音传播的路径的总长度。用这里得出的总长度和士兵的听觉范围作比较(听觉范围是设定好的),如果在其听觉范围内,则士兵会去声源位置寻找玩家(注意这里只是寻找,并没有看见玩家)。

这里写图片描述

这个方法用来判断士兵是否“看”到了玩家。士兵身上加一个collider,范围就是他的可视距离,这个脚本挂在士兵身上,当有碰撞体澎湖在和士兵身上的collider接触时进行判断,如果该碰撞体是玩家,并且从士兵身上射向玩家的射线(这里可以看作是视线)没有被别的东西遮挡住,那么士兵就“看”到了玩家。

以上代码都是官网stealth示例中的。

2020-02-25 23:31:07 valada 阅读数 59
  • Unity 值得看的500+ 技术内容列表

    Unity3D是由Unity Technologies开发的一个让玩家轻松创建诸如三维视频游戏、建筑可视化、实时三维动画等类型互动内容的多平台的综合型游戏开发工具,是一个全面整合的专业游戏引擎。

在Unity3d的一些2d游戏制作上,有时需要敌人在众多箱子(障碍物)中向主角移动,如果是简单的移动ai代码实现,敌人有可能陷入死胡同然后左右移动,卡死在胡同上,为了一个智能的ai实现,比较常用的是A*算法,但这里为了简便,我们使用了深度搜索的方式进行判断。所谓深度搜索,一位博主总结得很好,就是“不见棺材不回头。算法会朝一个方向进发,直到遇到边界或者障碍物,才回溯。一般在实现的时候,我们采用递归的方式来进行,也可以采用模拟压栈的方式来实现。”

这里给大家介绍一种用c#写的代码算法,希望能帮到大家

阅读全文: http://gitbook.cn/gitchat/activity/5e54fe92e577af7e239ba463

您还可以下载 CSDN 旗下精品原创内容社区 GitChat App ,阅读更多 GitChat 专享技术内容哦。

FtooAtPSkEJwnW-9xkCLqSTRpBKX

2018-03-12 22:12:19 qq_36927190 阅读数 1494
  • Unity 值得看的500+ 技术内容列表

    Unity3D是由Unity Technologies开发的一个让玩家轻松创建诸如三维视频游戏、建筑可视化、实时三维动画等类型互动内容的多平台的综合型游戏开发工具,是一个全面整合的专业游戏引擎。

一、解决问题:

在Unity3d的一些2d游戏制作上,有时需要敌人在众多箱子(障碍物)中向主角移动,如果是简单的移动ai代码实现,敌人有可能陷入死胡同然后左右移动,卡死在胡同上,为了一个智能的ai实现,比较常用的是A*算法,但这里为了简便,我们使用了深度搜索的方式进行判断。所谓深度搜索,一位博主总结得很好,就是“不见棺材不回头。算法会朝一个方向进发,直到遇到边界或者障碍物,才回溯。一般在实现的时候,我们采用递归的方式来进行,也可以采用模拟压栈的方式来实现。

在这里的代码中,是利用c#的栈,保存了名为Cell的宫格类,Cell类中包含了周围四个方向是否有路的信息,Maze迷宫类中将对每个能走的方向都进行尝试行走,将能走的路一直走下去,直到走不通为止,此时开始回溯,即使用stack.Pop(),一直退回分支,走下一条路线。举个例子:当前宫格的北方向【有路】,那么尝试向北走一步,走完后把设置此宫格的北方向为【无路】,并把走到的那个宫格的南方向设置为【无路】(都是相对的),就这样一直走下去,当发现无法到达终点时,开始退回,再次判断当前宫格的四个方向是否有路,直到退回到最后的分支上,进行下一条路的尝试行走。

该算法虽然简单,但却在不少地方都需要用到,比如游戏按键刷图脚本的地图判断的处理部分:例如找色判断dnf、手游《永远的7日之都》的万神殿地图部分,只要能直到当前揭示的地图,就可以算出一条通向boss的道路,然后自动刷图就完成了一个核心功能。


二、演示效果:

控制台程序代码Program.cs中输入:

 int[,] maps = new int[,] {
                { 0, 0, 0, 0, 0 },
                { 1, 0, 0, 1, 1 },
                { 0, 0, 0, 1, 0 },
                { 0, 1, 1, 1, 1 },

                { 0, 0, 0, 0, 0 } };

注释:0代表可通过,1代表不可通过。

运行程序,输出如下:

2 2 2 0 0
1 2 2 1 1
2 2 2 1 0
2 1 1 1 1
2 2 2 2 2

注释:2代表为走出的路径

三、上代码:


创建控制台程序:

Program.cs:

using System;

namespace MazePath
{
    class Program
    {
        static void Main(string[] args)
        {
            //0表示可走,1表示不可走
            int[,] maps = new int[,] {
                { 0, 0, 0, 0, 0 },
                { 1, 0, 0, 1, 1 },
                { 0, 0, 0, 1, 0 },
                { 0, 1, 1, 1, 1 },
                { 0, 0, 0, 0, 0 } };


            Maze maze = new Maze(maps);           
            Console.ReadLine();
        }
    }
}

Maze.cs:

using System;
using System.Collections.Generic;


namespace MazePath
{
    class Maze
    {
        Cell[,] cells;
        Stack<Cell> cellStack = new Stack<Cell>();
        int[,] maps;
        int rows;
        int length;

        public Maze(int[,] maps)
        {
            this.maps = maps;
            length = maps.GetLength(1);
            rows = maps.GetLength(0);
            cells = new Cell[rows, length];
            InitCells();
            GetValidPath();
            ConsoleResult();
        }

        public void InitCells()
        {
            for (int i = 0; i < rows; i++)
            {
                for (int j = 0; j < length; j++)
                {
                    //设置每个格子自身的位置属性
                    Cell cell = new Cell();
                    cell.SetPos(i, j);

                    cells[i, j] = cell;

                    //-----设置格子的周围方向属性---------------
                    //西
                    if ((j - 1) >= 0)
                    {
                        if (maps[i, j - 1] == 0)
                            cells[i, j].SetDirection(Const.WEST, Const.ENABLE);
                        else
                            cells[i, j].SetDirection(Const.WEST, Const.DISABLE);
                    }

                    //东
                    if ((j + 1) < length)
                    {
                        if (maps[i, j + 1] == 0)
                            cells[i, j].SetDirection(Const.EAST, Const.ENABLE);
                        else
                            cells[i, j].SetDirection(Const.EAST, Const.DISABLE);
                    }

                    //北
                    if ((i - 1) >= 0)
                    {
                        if (maps[i - 1, j] == 0)
                            cells[i, j].SetDirection(Const.NORTH, Const.ENABLE);
                        else
                            cells[i, j].SetDirection(Const.NORTH, Const.DISABLE);
                    }

                    //南
                    if ((i + 1) < rows)
                    {
                        if (maps[i + 1, j] == 0)
                            cells[i, j].SetDirection(Const.SOUTH, Const.ENABLE);
                        else
                            cells[i, j].SetDirection(Const.SOUTH, Const.DISABLE);
                    }

                }
            }
        }

        public void GetValidPath()
        {
            int i = 0;int j = 0;
            cellStack.Push(cells[i, j]);
            while (true)
            {
                i = cellStack.Peek().x; j = cellStack.Peek().y;
                if (i == (rows - 1) && j == (length - 1))
                {
                    return;
                }

                //东
                if (cellStack.Peek().GetDirection(Const.EAST)==Const.ENABLE)
                {
                    cells[i, j].SetDirection(Const.EAST, Const.DISABLE);
                    cells[i, j+1].SetDirection(Const.WEST, Const.DISABLE);
                    cellStack.Push(cells[i, j + 1]);
                    continue;
                }
                //西
                if (cellStack.Peek().GetDirection(Const.WEST) == Const.ENABLE)
                {
                    cells[i, j].SetDirection(Const.WEST, Const.DISABLE);
                    cells[i, j + 1].SetDirection(Const.EAST, Const.DISABLE);
                    cellStack.Push(cells[i, j - 1]);
                    continue;
                }
                //北
                if (cellStack.Peek().GetDirection(Const.NORTH) == Const.ENABLE)
                {
                    cells[i, j].SetDirection(Const.NORTH, Const.DISABLE);
                    cells[i - 1, j].SetDirection(Const.SOUTH, Const.DISABLE);
                    cellStack.Push(cells[i-1, j]);
                    continue;
                }
                //南
                if (cellStack.Peek().GetDirection(Const.SOUTH) == Const.ENABLE)
                {
                    cells[i, j].SetDirection(Const.SOUTH, Const.DISABLE);
                    cells[i + 1, j].SetDirection(Const.NORTH, Const.DISABLE);
                    cellStack.Push(cells[i+1, j]);
                    continue;
                }

                cellStack.Pop();

                if (cellStack.Count == 0)
                {
                    Console.WriteLine("根本没有这个路径");
                    return;
                }
            }
        }

        public void ConsoleResult()
        {
            while (cellStack.Count != 0)
            {
                int i = cellStack.Peek().x;
                int j = cellStack.Peek().y;
                cellStack.Pop();
                maps[i, j] = 2;
            }

            string s = "";
            for(int i = 0; i < maps.GetLength(0); i++)
            {
                for(int j = 0; j < maps.GetLength(1); j++)
                {
                    s += maps[i, j].ToString()+" ";
                }
                s += "\r\n";
            }
            Console.WriteLine(s);
        }
    }
}

Cell.cs:

namespace MazePath
{
    class Cell
    {
        public int x;
        public int y;
        //directon 索引0-3分别是东西南北,默认为0表示不可走,1则表示可走
        private int[] directions = new int[4];

        public void SetPos(int x,int y)
        {
            this.x = x;
            this.y = y;
        }

        public bool IsEnable(int index)
        {
            return directions[index]==1;
        }
        public void SetDirection(int index,int result)
        {
            directions[index] = result;
        }
        public int GetDirection(int index)
        {
            return directions[index];
        }
    }
}

Const.cs:

namespace MazePath
{
    public static class Const
    {
        public const int EAST = 0;   //东
        public const int WEST = 1;   //西
        public const int SOUTH = 2;  //南
        public const int NORTH = 3;  //北

        public const int ENABLE = 1;
        public const int DISABLE = 0;

    }
}

2017-03-06 18:17:27 Ming__Liang 阅读数 2488
  • Unity 值得看的500+ 技术内容列表

    Unity3D是由Unity Technologies开发的一个让玩家轻松创建诸如三维视频游戏、建筑可视化、实时三维动画等类型互动内容的多平台的综合型游戏开发工具,是一个全面整合的专业游戏引擎。

记录下关于打斗类(如:ARPG类,并非格斗类)游戏的攻击判定问题,通常这类游戏都会有普通攻击和技能攻击等攻击方式(可参考王者荣耀),下面将分别介绍下。


一、普通攻击

    普通攻击的流程就是主角靠近敌人,播放攻击动画,调用敌人受伤害计算方法(+被击动画、特效等);这一过程有几点需要注意的,

    (1)调用受伤害计算方法时机:因为播放攻击动画会有1s左右的时间,可以在播放动画同时启动一个协程来帮助调用受伤害计算方法;

    (2)攻击成功触发条件:通过点击攻击按钮可以触发攻击技能,但不代表能打出攻击伤害,主角和敌人必须满足一些条件才行;通常我们的做法是判断距离和方向。通俗来说,比如主角要攻击前面的敌人,从这就可以提取两点信息了,就是两者需满足一定的距离和角度了。具体方法如下:

// 方式1:通过主角和场景中的所有敌人比较
private void AtkCondition1(float _range,float _angle)
{
    // 搜索所有敌人列表(在动态创建敌人时生成的)
    // 列表存储的并非敌人的GameObject而是自定义的Enemy类
    // Enemy类的一个变量mGameObject则用来存储实例出来的敌人实例
    foreach (var go in GameManager.GetInstance.gMonsterDict)
    {
        // 敌人的坐标向量减去Player的坐标向量的长度(使用magnitude)
        float tempDis1 = (go.Value.mGameObject.transform.position - mGameObject.transform.position).magnitude;
        // 敌人向量减去Player向量就能得到Player指向敌人的一个向量
        Vector3 v3 = go.Value.mGameObject.transform.position - mGameObject.transform.position;
        // 求出Player指向敌人和Player指向正前方两向量的夹角,其实就是Player和敌人的夹角(不分左右)
        float angle = Vector3.Angle( v3, mGameObject.transform.forward);
        if (tempDis1 < _range && angle < _angle)
        {
            // 距离和角度条件都满足了
        }
    }
}
// 方式2:通过主角和射线检测到的敌人比较
private void AtkCondition2(float _range,float _angle)
{
    // 球形射线检测周围怪物,不用循环所有怪物类列表,无法获取“Enemy”类
    Collider[] colliderArr = Physics.OverlapSphere(mGameObject.transform.position, _range, LayerMask.GetMask("Enemy"));
    for (int i = 0; i < colliderArr.Length; i++)
    {
        Vector3 v3 = colliderArr[i].gameObject.transform.position - mGameObject.transform.position;
        float angle = Vector3.Angle(v3, mGameObject.transform.forward);
        if (angle < _angle)
        {
            // 距离和角度条件都满足了
        }
    }
}

上面两种方式主要针对两种不同方式,第一种方式是因为我的主角、敌人等对象是不挂任何脚本的,所有的模型对象都是动态生成,模型对象只是对应类(Player类和Enemy类等)中的一个变量而已,所以需要循环查找Enemy类列表来获取对应的其中一个类实例,这样就不单能获取GameObject了,而是可以获取Enemy类中的任意公开数据(变量、方法等);但这种方式也有个小问题,举个例子:故事背景是军队打仗,双方各100人;那么使用方式1就是双方各有100个类实例,而每个类实例都包含这个判断方法,是循环对方类实例列表(大小100),那么双方加起来就是(2*100*100)的计算量了,当然手游的话不应该这么极端同时出现这么多模型的。即使是一个主角加100的情况下,主角类作这样的判断也是浪费的,因为一般主角旁边最多几个敌人的,不应该每次都查找所有敌人啊。

    所以第二种方式就是这种情况,只判断身旁的敌人,通过主角发射一定长度的环形射线检测周围敌人(类似球形触发器检测敌人是否进入触发器),直接获取射线检测到的敌人数组列表,再将其和主角作夹角对比,从而得到判断结果。因为碰撞检测都是直接得到碰撞对象GameObject的,比较适合对象上挂载脚本的方式(获取数据方便),但是对于我那种方式来说,我如果要通过一个GameObject获取其所属的类实例,只能循环查找类实例列表一个个判断了,那么就又变回第一种方式了,所以说这两种方式应该按实际情况去使用


技术扩展

    (1)归一化、点乘、叉乘

    1.1 上面我们用了方法Vector3.Angle来求两向量的夹角,其实也是可以用其他方法来计算的。

// 计算目标是否在指定扇形攻击区域内
private bool CalculateDistance()
 {
     float distance = (mGameObject.transform.position - Target.transform.position).magnitude;
     Vector3 mfrd = mGameObject.transform.forward;
     Vector3 tV3 = Target.transform.position - mGameObject.transform.position;
     // mfrd.normalized(归一化):方向不变,长度归一,用在只关心方向忽略大小情况下(毕竟以单位1计算比使用float类型数计算方便快速嘛)
     // Vector3.Dot(点乘):余弦值;Mathf.Acos()反余弦值(弧度形式表现)
     // Mathf.Rad2Deg:弧度转度;Mathf.Deg2Rad:度转弧度
     float deg = Mathf.Acos(Vector3.Dot(mfrd.normalized, tV3.normalized)) * Mathf.Rad2Deg;
     // 一半扇形区域
     if (distance < 2f && deg < 120 * 0.5){
        return true;
     }
     return false;
 }

    1.2 点乘虽然可以判断两坐标点的夹角,但范围是[0,180],也就是说不分左右的,那么在实现移动转身时就无法区分顺时针转身或者逆时针转身了(只能指定一个转身方向)。

// 借用上面变量作叉乘计算
Vector3 t = Vector3.Cross(mfrd.normalized, tV3.normalized); // 叉乘结果为一个Vector3向量

    关于叉乘结果t,有三种结果:如果t.y>0 目标点在主角右方,主角顺时针转身;如果t.y<0 目标点在主角左方,主角逆时针转身;t.y=0,两者平行(夹角0或180)。PS:为方便记忆,可使用左手定则,如果y>0,左手拇指朝上,四指为转身方向,否则相反。


    当然也可以使用下面方法自带区分顺、逆方向旋转

Quaternion rotation = Quaternion.LookRotation(TargetPoint - mGameObject.transform.position);
mGameObject.transform.rotation = Quaternion.Slerp(mGameObject.transform.rotation, rotation, Time.deltaTime * 10f);

没有更多推荐了,返回首页