自制Unity小游戏TankHero-2D(1)制作主角坦克

我在做这样一个坦克游戏,是仿照(http://game.kid.qq.com/a/20140221/028931.htm)这个游戏制作的。仅为学习Unity之用。图片大部分是自己画的,少数是从网上搜来的。您可以到我的github页面(https://github.com/bitzhuwei/TankHero-2D)上得到工程源码。

本篇主要记录制作主角坦克(TankHero)的一些重点。

2D游戏布局

如上图所示,红色矩形围起来的是主角坦克,白色的一圈是围墙,坦克和围墙在同一平面上。地面背景放到离摄像机最远的后方。这样,在2D摄像机下看起来是这样的:

坦克本身由底座(Base)和炮塔(Head)两部分组成。当然,在2D世界,其实就是两个扁平的贴图。在2D摄像机下是这样的:

(PS:上图中的绿色矩形框是Box Collider 2D,忽略即可)

为了保证炮塔始终显示在底座上方,我们要让炮塔稍微靠近一点摄像机。如下图所示,炮塔和底座两张贴图是分隔开的。

坦克的运动

坦克的运动包括:上下左右平移;底座旋转;炮塔旋转。其中平移时会同样地移动底座和炮塔,所以用最上层的TankHero负责。底座和炮塔的旋转我们要求两者互不干涉,所以TankHead和TankBase放在同一层,并且分别负责各自的旋转。

移动

坦克的移动十分容易。玩家在纵横方向的按键情况就是坦克的移动方向,速度由程序员指定,再乘上时间就好了。

 1     void Update () {
 2         var h = Input.GetAxis ("Horizontal");
 3         var v = Input.GetAxis ("Vertical");
 4
 5         if (Mathf.Abs(h) > Quaternion.kEpsilon || Mathf.Abs(v) > Quaternion.kEpsilon)
 6         {
 7             Move (h, v);
 8         }
 9     }
10
11     void Move(float h, float v)
12     {
13         var moveVector = new Vector3 (h, v, 0);
14         moveVector.Normalize ();
15         this.transform.position += moveVector * speed * Time.deltaTime;
16     }

底座旋转

底座应该朝向移动的方向,即上文的 moveVector 。这里用 Quaternion.Slerp 使底座平滑地转向 moveVector 。

 1     void Update () {
 2         var h = Input.GetAxis ("Horizontal");
 3         var v = Input.GetAxis ("Vertical");
 4
 5         if (Mathf.Abs(h) > Quaternion.kEpsilon || Mathf.Abs(v) > Quaternion.kEpsilon)
 6         {
 7             this.targetAngle = Mathf.Atan2(v, h) * Mathf.Rad2Deg;
 8             //Debug.Log("target angle: " + targetAngle);
 9         }
10
11         this.transform.rotation = Quaternion.Slerp (
12             this.transform.rotation,
13             Quaternion.Euler (0, 0, targetAngle),
14             rotationSpeed * Time.deltaTime);
15     }

炮塔旋转

炮塔要指向鼠标(即目标)所在的位置,所以从炮塔到鼠标的向量就是炮塔的方向。

注意炮塔不是围绕自身的中心旋转的,这个旋转点需要根据坦克的形状来指定。所以这里要用 transform.RotateAround 来进行旋转。

 1     void Update () {
 2         Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
 3         RaycastHit hit;
 4         if(Physics.Raycast(ray, out hit))
 5         {
 6             var p = hit.point;
 7             var y = p.y - this.transform.position.y;
 8             var x = p.x - this.transform.position.x;
 9             if (Mathf.Abs(y) > Quaternion.kEpsilon || Mathf.Abs(x) > Quaternion.kEpsilon)
10             {
11                 this.targetAngle = Mathf.Atan2(y, x) * Mathf.Rad2Deg;
12                 var angle = this.targetAngle - this.transform.rotation.eulerAngles.z;
13
14                 this.transform.RotateAround (this.rotationCenter.position, new Vector3 (0, 0, 1), angle);
15             }
16         }
17     }

车轮滚动

其实这不算是运动了,不过放在这一节也还算紧凑。

坦克移动的时候,我希望车轮下下图所示这样,显得很生动:

我的思路是用4张图片表现车轮滚动的效果,让TankBase负责循环显示这4张图片。

当然,脚本可以处理任意多张图片的循环播放。其关键就是依次将各个BaseSprite的 renderer.enabled 字段设为 true 。

 1     public float interval = 10;
 2     public List<GameObject> wheels;
 3     private int current = 0;
 4     private float passedInterval = 0;
 5
 6     // Use this for initialization
 7     void Start () {
 8         if (wheels != null && wheels.Count > 0)
 9         { wheels[0].renderer.enabled = true; }
10
11         for (int i = 1; i < wheels.Count; i++)
12         {
13             wheels[i].renderer.enabled = false;
14         }
15     }
16
17     // Update is called once per frame
18     void Update () {
19         if (wheels == null || wheels.Count < 2) { return; }
20
21         var h = Input.GetAxis ("Horizontal");
22         var v = Input.GetAxis ("Vertical");
23
24         if (Mathf.Abs(h) > Quaternion.kEpsilon || Mathf.Abs(v) > Quaternion.kEpsilon)
25         {
26             passedInterval += Time.deltaTime * 100;
27             //Debug.Log (passedInterval);
28             if (passedInterval >= interval)
29             {
30                 var tmp = current;
31
32                 if (current == wheels.Count - 1) { current = 0; }
33                 else { current++; }
34
35                 wheels[current].renderer.enabled = true;
36                 wheels[tmp].renderer.enabled = false;
37                 passedInterval = 0;
38             }
39         }
40     }

车轮滚动

坦克开炮

这个游戏中,TankHero能够发射多种炮弹,所以需要有多种武器,每种武器发射一种炮弹。因此炮塔充当了武器管理员的角色,而不是武器本身。一种武器决定了它发射的炮弹的速度、威力等信息。这段话是武器系统的关键。

发射炮弹这种事,典型的方法是用 Instantiate 。这就需要在场景中持有一个现成的炮弹。如下图所示:

这个炮弹要永远存在,还不能被摄像机看到,所以我们把它放到之前说的地面背景的更后面。

你注意到图中的炮弹中心有个比较小的绿色的圈,这个圈是Circle Collider 2D,是用来产生碰撞的。我刻意把这个Collider调到这么小,是为了避免在坦克刚刚发射出炮弹时,炮弹与自身产生碰撞(即自己开炮瞬间打了自己)。

同时,在上图中我用黄色圈圈出了那个BulletPosition的gameobject,这是专门用来指定炮弹产生点的,也是为了避免炮弹刚刚发射出来就把自己给打了。

注意,带2D的Collider似乎有这样的问题:无论在Z方向上是否在同一Z平面,都能引发碰撞事件。所以,那个永生的炮弹,虽然藏到地面背景后方去了,却仍旧可能与游戏中的其它物体发生碰撞(然后就会爆炸消失被Destroy掉,之后就无法再用Instantiate来创建炮弹了)。为了避免它的 Destroy ,我们需要将它和其它炮弹区别开来,所以就必须给炮弹对象添加一个 undying 字段,让 undying 为true的炮弹在触发了碰撞事件时也不爆炸消失。

摄像机随主角移动

我希望地图能够大一点,所以一屏肯定放不下。所以需要摄像机随主角坦克的移动而移动。这个很容易,不断跟随主角坦克就行了。

 1     public float catchingSpeed = 1;
 2     private Transform tankHero;
 3
 4     void Awake()
 5     {
 6         this.tankHero = GameObject.FindGameObjectWithTag (Tags.hero).transform;
 7     }
 8     void Update () {
 9         var targetPosition = new Vector3 (this.tankHero.position.x, this.tankHero.position.y, this.transform.position.z);
10         this.transform.position = Vector3.Lerp (this.transform.position, targetPosition, Time.deltaTime * this.catchingSpeed);
11     }

注意这里将 catchingSpeed 调低一些,会产生摄像机延迟跟随主角坦克的现象。我很喜欢这种跟随的感觉,柔和不生硬,而且还解决了后文遇到的一个问题。

自定义鼠标样式

我希望鼠标在游戏中显示为下图所示的样子,很带感。

方法有两种。

Default cursor

一是在File – Build Settings – Player Settings打开的Inspector面板中设置Default Cursor。

这个方法有点问题,首先在build之后的exe中你可能发现鼠标彻底消失了,既没有原始图标也没有自定义图标,其次在你修改了自定义图标之后,可能会显示成一个很奇怪的图标,最后,这样自定义的图标,其清晰度大打折扣,其size也是固定的。

所以我推荐另一种方法,即用脚本实现。

脚本实现

典型的实现方式是这样的,在主摄像机上添加一个TargetCusor.cs的脚本(脚本名无所谓),编写代码如下:

 1     //3D贴图是Material,2D贴图是Texture
 2     public Texture CurosrTexture;
 3     void OnGUI() { //    渲染GUI和处理GUI时调用。
 4         if (CurosrTexture != null) {
 5             // 计算图片左上角的坐标
 6             float left = Input.mousePosition.x - CurosrTexture.width / 2;
 7             float top = Screen.height - Input.mousePosition.y - CurosrTexture.height / 2;
 8
 9             GUI.DrawTexture(new Rect(left, top, CurosrTexture.width, CurosrTexture.height), CurosrTexture);
10         }
11     }

在Inspector面板中指定你的图标即可。

围墙

限制坦克和炮弹的活动范围是必须的。这里我暂且简单地制作一个正方形围墙。

这个围墙由四个quad组成。绿色的线条是Box Collider 2D组件。围墙的功能就是把撞上它的东西(坦克、炮弹等)弹回去。这里不得不用一个 Dictionary<Collider2D, Vector3> 字典记录撞到围墙的物体在碰撞瞬间的位置,因为之后要将物体弹回这个位置。

 1 public class PushBackToField : MonoBehaviour {
 2     Dictionary<Collider2D, Vector3> initialPositionDict;// = new Dictionary<Collider, Vector3>();
 3
 4     void Awake()
 5     {
 6         initialPositionDict = new Dictionary<Collider2D, Vector3> ();
 7     }
 8
 9     void OnTriggerEnter2D(Collider2D other)
10     {
11         if (initialPositionDict.ContainsKey(other))
12         {
13             initialPositionDict[other] = other.transform.position;
14         }
15         else
16         {
17             initialPositionDict.Add(other, other.transform.position);
18         }
19     }
20
21     void OnTriggerStay2D(Collider2D other)
22     {
23         Push (other);
24     }
25
26     void OnTriggerExit2D(Collider2D other)
27     {
28         if (initialPositionDict.ContainsKey(other))
29         {
30             initialPositionDict.Remove(other);
31         }
32     }
33
34     void Push(Collider2D other)
35     {
36         Vector3 initialPosition = Vector3.zero;
37         if (initialPositionDict.ContainsKey(other))
38         {
39             initialPosition = initialPositionDict[other];
40         }
41         else
42         {
43             Debug.LogError(string.Format("{0} should have been added to the dict.", other.gameObject.name));
44         }
45
46         if ((initialPosition - other.transform.position).magnitude > 0.001f)
47         {
48             //Debug.Log("lerp push");
49             other.transform.position = Vector3.Lerp(other.transform.position, Vector3.zero, Time.deltaTime);
50         }
51         else
52         {
53             //Debug.Log("sudden push");
54             other.transform.position = initialPosition;
55         }
56     }
57 }

围墙

这个脚本对上下左右四个围墙都适用,以后有了别的形状的围墙,也仍然适用。这也是它的优点之一。

说到这个围墙的反弹,就涉及摄像机跟随的一个问题。实际上,围墙反弹时,如果玩家持续撞击围墙,会使玩家坦克产生快速的震动。此时摄像机也就跟着快速震动,这很影响体验。上文里将跟随速度设置得比较低时,这种震动就不会影响到摄像机。这是因为,摄像机反应慢,震动速度快,不等摄像机需要向左跟随,就又要向右跟随了,所以摄像机基本上就在原地不动了。

将上文的 catchingSpeed 调大一些,再持续去撞墙,你就会明白了。

光源

忘了说,要添加一个线光源,不然场景会很暗淡。下图就是没有添加光源的样子。

加上光源就成了这样:

显示文字

想显示上图所示的文字?用Unity最近推出的uGUI还是很舒服的(也可能是因为我没有学过nGUI等等的UI系统吧)。

点击Text后,会增加3个对象,Canvas,Text和EventSystem。

给Text对象添加一个DrawMouseInfo.cs的组件(名字无所谓)。

 1 public class DrawMouseInfo : MonoBehaviour {
 2     Text guiText;
 3
 4     void Awake()
 5     {
 6         guiText = this.GetComponent<Text> ();
 7     }
 8
 9     void Update () {
10         Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
11         RaycastHit hit;
12         if(Physics.Raycast(ray, out hit))
13         {
14             guiText.text = string.Format ("input: {0} mouse: {1} | {2}", Input.mousePosition, hit.point, hit.transform.gameObject.name);
15         }
16         else
17         {
18             guiText.text = string.Format ("input: {0} mouse: {1} | {2}", Input.mousePosition, "null", "null");
19         }
20     }
21 }

uGUI对象是可以在Scene视图里拖动的,只不过你要先找到它。

它的位置很奇葩,如上图所示,整个地图在它的Canvas脚下都很渺小。

快速自制贴图资源

本项目中的坦克、子弹、光标、背景图都是本人制作的,制作工具你猜猜?是PPT。

坦克底座是SmartArt图形里的。

轮子只是设置了一下渐变填充。

炮塔的圆形,把底座的圆形缩小一点就是。炮塔的炮管,是“形状”里的箭头,删掉凸起的尖的部分,调整一下锚点长短就OK。

背景用的是“纹理填充”,看到第二行第一个了没?

准星,用的是SmartArt里的“分离射线”。把四个箭头留下,其它内容删除。再把箭头的尾部顶点删除,左右交换位置,上下交换位置,上个色就成了。

还可以吧?

总结

您可以到我的github页面(https://github.com/bitzhuwei/TankHero-2D)上得到工程源码。

请多多指教~

转载于:https://www.cnblogs.com/bitzhuwei/p/tank-hero-2d-1-make-tank-hero.html

自制Unity小游戏TankHero-2D(1)制作主角坦克相关推荐

  1. 自制Unity小游戏TankHero-2D(3)开始玩起来

    自制Unity小游戏TankHero-2D(3)开始玩起来 我在做这样一个坦克游戏,是仿照(http://game.kid.qq.com/a/20140221/028931.htm)这个游戏制作的.仅 ...

  2. python自制小游戏_教你用Python自制拼图小游戏,一起来制作吧

    摘要: 本文主要为大家详细介绍了python实现拼图小游戏,文中还有示例代码介绍,感兴趣的小伙伴们可以参考一下. 开发工具 Python版本:3.6.4 相关模块: pygame模块: 以及一些Pyt ...

  3. unity小游戏制作之见缝插针

    unity小游戏制作之见缝插针 先看效果图这个游戏是一个入门的小游戏,主要技术就是圆盘的旋转,针跟随圆盘旋转以及分数累加等 游戏先拖入一个圆,更改格式为2D然后建立脚本使其转动 public clas ...

  4. Unity小游戏-彩笔画师(安卓、PC)2D益智类游戏 项目展示+完整项目源码

    游戏录像 游戏玩法 谁画在屏幕上占的颜色最多,谁获胜. 功能 1.游戏有多个关卡,每个关卡的人机难度不一致. 2.可以存金币购买角色,不同角色的技能和属性不一样. 3.每个关卡的评分等数据存到本地. ...

  5. Unity小游戏-平衡大师(安卓、PC、web)2D益智类游戏 项目展示+完整项目源码

    游戏录像 游戏试玩 平衡带师_平衡带师html5游戏_4399h5游戏-h.4399.com 游戏玩法 这是一款类似堆积木的游戏,它非常考验玩家的智商和手速,难度系数很高的游戏,谁能征服它,谁就是平衡 ...

  6. 帽子接球小游戏(一)--制作UI面板

    帽子接球小游戏(一)--制作UI面板 注:本小游戏共两章,第一章讲界面的制作,第二章讲后台代码控制接球.最终效果见下章. (工程中所使用到的所有图片都是我自己PS画的,比较粗糙,不太美观,能看就行,哈 ...

  7. 2022unity超简单课设-模拟太阳系的Unity小游戏

    模拟太阳系的Unity小游戏(附下载链接) 下载链接 unity课程实践做的一个模拟太阳系的Unity小游戏,你可以使用飞船移动来查看太阳系中的各个星球. 飞船拥有两种驾驶模式,一种更加真实,是通过给 ...

  8. Unity 小游戏:3D射箭

    Unity 小游戏:3D射箭 前两周因为实训太忙,再加上自己对老师所讲的设计模式并不是很理解,所以就没有写博客.这次博客是记录3D射箭游戏的实现过程. Unity 小游戏3D射箭 准备资源 布置场景 ...

  9. Unity小游戏(一)——Unity JigsawPuzzle(拼图游戏)

    1.前言: 简单的Unity小游戏,切割图片,生成随机区块,拖拽交换位置. 此游戏代码只贴了一小部分,具体可见GitHub:源码 效果: 2.素材准备 简单做了下,所以没有用太多素材,只准备了两种字体 ...

最新文章

  1. 涵盖18+ SOTA GAN实现,这个图像生成领域的PyTorch库火了
  2. java后端经验和技术总结(1)
  3. PAT甲级题目翻译+答案 AcWing(高精度)
  4. windows如何访问linux系统文件,如何从 Windows 访问 Linux 文件?——方法步骤
  5. php 需要已安装且正在运行的邮件系统_php如何发送邮件?一个函数轻松搞定
  6. (旧)走遍美国——(三、文化1)
  7. 链式栈的实现(头文件及源程序)
  8. opencv对图像是软解码_C ++ OpenCV解码缓慢
  9. Linux: find和xargs用法整理
  10. 震惊!腾讯要建AI鹅厂,百度让狗刷脸购物,锤子R-1真机披露
  11. Rocket - decode - 最小项与最大项
  12. 浅析python的metaclass
  13. IBM为北约创建云计算系统用于军情分析
  14. 文本搜索引擎lucene
  15. 【C++】优先级队列priority_queue模拟实现仿函数
  16. Redhat认证体系
  17. android 读取sdcard图片 不显示,Android 读取sdcard上的图片实例(必看)
  18. 指纹支付 java lang6_支付宝;超6成人使用指纹、刷脸支付,网友;喜欢用密码!...
  19. 流程图中的实线_化工工艺流程图中的设备用细实线画出,主要物料流程线用粗实线画出。()_搜题易...
  20. 北京 上海 天津 河北 融资性担保机构经营许可证

热门文章

  1. CentOS7部署nextcloud最新版本
  2. (附源码)spring boot信佳玩具有限公司仓库管理系统 毕业设计 011553
  3. C语言——自定义类型(结构体,枚举,联合体,位段)
  4. PTA(每日一题)7-75 某校几人
  5. 华为机试题 - 求一个数的质数解(java实现)
  6. javascript打印直角三角形四种情况
  7. Python 使用office365邮箱自动发送邮件
  8. 5G毫米波终端关键技术分析
  9. 认识网络机柜布线中跳线架和配线架的用途
  10. Camera结构原理