一、美工相关
因为2D sprite图片使用rotate翻转之后貌似碰撞器不会随之改变位置,所以使用scale * -1 来实现翻转,这样一来,3渲2出来的图片最好关于中心轴对称,否则翻转之后会出现如下问题:
从游戏画面角度来说这个人物就好像瞬移旋转一样。
使用程序初步解决了这个问题,在翻转前先往预定的方向移动一段距离来抵消翻转造成的瞬移。
二、场景相关
场景部分:
水果buff 产生两次:因为人物有两个colider component,所以产生两次trigger enter event.
人物运动部分
人物采用2D rigidbody component 的时候,才冰块组成的Wall上移动的时候,如果使用box colider 2d 碰撞器,由于物理模拟造成的棱角,导致运动的的中端。这个问题原因寻找了好久,因为当时的ice wall 拼接的时候非常严密,最后使用circle colider 2d 碰撞器解决这个问题。
三、构架相关、
后期重构参考:
考虑地图,怪物,Boss在多个关卡有着惊人的相似度,如果考虑后期重构可以用一个场景来实现6个关卡,使用脚本动态加载6个关卡的不同个元素,6个关卡之间共享相同的元素。比如相同的怪物与Boss控制脚本等等,只在渲染上做动态加载。
对象池:
工作流程如下:
注意,如果你采用了对象池来实现一个重复使用对象的方案,那么,当你把对象使用完毕后放入池子里的时候,拿出来时要注意恢复成你最初开始拿出来的样子,也就是恢复对象的变量,恢复对象的状态,断开与其他脚本的联系等等(当然你也可以再放入的时候恢复,这些都是一样的)
总之一句话,你使用对象池拿出来容易,再放回去没这么容易,注意要恢复成原来的样子,最好自己内部脚本有一个放回的方法,里面实现一些恢复,然后由控制器来调用放回,比如与单例脚本断开链接,恢复子物体的样子。
同样的,如果你产生也有一些初始化工作,我建议你也在内部脚本中实现,然后由控制器调用。(也就是在对象池产生回收的时候调用)
你必须要记住的是OnEnable,OnDestory ,OnDisable 这些回调函数只是在物体active 变换的时候调用,本质上并不是对象池产生回收的时候调用的。这两者不是等价的。
举个例子,OnEnable联系单例脚本,OnDisable断开单例脚本,退出编辑器,OnDisable 中访问已经销毁的单例脚本,是不正确的。
又或者冰块被碰撞 active = false的时候你在单例脚本中记录下其坐标,那么你退出编辑器或者父物体set active false的时候同样也会有这些逻辑操.
再或者当你生成对象池的时候,你却要在OnEnable OnDisable的时候进行对象初始化,完全没必要,浪费性能。
本质上也好分析,这些函数也不是为了你回收创建的。
总之对象池中的对象初始化和销毁工作功能写在对象自己的脚本中,由控制器调用,而不是系统函数调用,当然你也可以设定过一段时间自己回收然后调用,本质也是一样的。
单例脚本:
单例脚本分为如果自己带有一些资源比如贴图,预设,像BuffManager,或者不带预设,类似人物与怪物交互之间的单例脚本(可以做距离判断)。
对于不是动态创建的单例脚本,如果访问单例脚本,不要一开始就访问, 因为你不能保证这个单例脚本会先于你之前创建。
PS:一般第一种脚本我都不是动态创建 ,因为资源不是动态加载。
如果需要一个跨场景并且带有资源的单例脚本,可以采用如下
当加载一个新关卡时,所有场景中所有的物体被销毁,然后新关卡中的物体被加载进来。 为了保持在加载新关卡时物体不被销毁,使用DontDestroyOnLoad保持, 如果物体是一个组件或游戏物体,它的整个transform层次将不会被销毁,全部保留下来。 // survive when loading a new scene. // Make this game object and all its transform children //当加载新场景的时候,使游戏物体和它所有的transform子物体存活下来 function Awake () { DontDestroyOnLoad (transform.gameObject); }
多平台信号控制
考虑到游戏的跨平台性,我们必须要适应不同的输入设备,这个时候,如果输入设备的检测放在逻辑代码里是不现实的,那么如果输入设备改了,我们的逻辑代码也要去改,会很麻烦。那么我们可以把这个输入设备封装出来,不同输入设备有不同输入设备的信号,我们可以用c# 的委托来根据不同输入设备进行监听。
引发对怪物的AI思考
怪物的AI可以分为传感器的输入,内部的逻辑处理,最后的对应的表现,我们可以把传感器的输入独立出来,传感器的检测逻辑由怪物的行为逻辑监听,行为逻辑就可以喝检测逻辑分开,松耦合。
四、脚本
一:Awake在Enable之前。
二、Time.scale = 0 的时候 游戏的声音不会停止播放,依旧会播放,比如游戏结束场景出来的时候,人物的的战斗声音如果比较长或者设置为loop,那么他就会一直播放。
三、www 类不能ref 传入
四、c#的委托对set active false的物体同样有效
五、DateTime.Parse的日期格式如果要和服务器交互,那么你需要to String后转换成服务器的日期格式 本地默认的格式是不能直接提交的。
项目中我都是先将本地的默认日期格式变为如下
string DateTime = String.Format("{0:u}", Globals.Me.VipTime.Value); //2010-10-04 17:38:01Z 再进行提交
否则服务器不认识本地默认的日期格式就会变为 0000-00-0 00:00:00 同时DateTime.Parse 不能解析0000-00-0 00:00:00 就会发生异常
日期格式大全如下:
http://www.cnblogs.com/shaocm/archive/2012/08/15/2639998.html
六、注意,如果你的按钮按下后会消失或者会加载场景,你这时候在按下的函数中处理服务器数据提交是不明智的,所以我们应该分层,等待数据提交完毕后按钮再消失。
五、游戏引擎
一、注意你不要在Animation Event 中试图 this.gameobject.SetActive(false),这会导致引擎奔溃。我花了差不多几个小时才找出来。
注: 7.12 追加
这个BUG在4.5 版本中已经修复
解决办法如下:
http://answers.unity3d.com/questions/590945/gameobjectsetactivefalse-crashes-unity.html
http://answers.unity3d.com/questions/595459/unity-43-crashes-setting-objects-inactive.html
Unity apparently does not like it when the gameObject the animationEvent happens on gets disabled in the middle of the event or something similar.
A workaround could be to use a coroutine to wait for the end of the frame instead of disabling immediately:
public void OnAnimationEnd() { StartCoroutine("Delay"); } private IEnumerator Delay() { yield return new WaitForEndOfFrame(); foreach (Transform level in levels.transform) { level.gameObject.SetActive(false); } }
或者Or only disable the SpriteRenderer to hide the Sprite:
private bool isAnimationCompleted; public void AnimationCompleted() { isAnimationCompleted = true; } void Update() { if(isAnimationCompleted) { isAnimationCompleted = false; gameObject.SetActive(false); } }
二、Sprite Packer 能不用就最好不用,他本身的在编译器运行的时候会计算一次,非常消耗内存,Draw Call 不到100不用搞这个。
下面这些人也碰到了这些问题
http://issuetracker.unity3d.com/issues/sprite-pack-crash
http://forum.unity3d.com/threads/sprite-packer-runs-out-of-memory.229999/
三、Application.persistentDataPath is empty
自己多个判断即可
static string FileName { get { if (Application.persistentDataPath.Length == 0) { return GameConfig.PresistentDataPath+"/game_9.dat"; } else { } return Application.persistentDataPath + "/game_9.dat"; } }
三、关于Unity3D内存加载
网上流程很广的那篇有关两篇Unity3D内存加载,看完之后,受益颇多,但是其实有几句话我亲自测试是有问题的,如下:
存在3种加载prefab的方式: 一是静态引用,建一个public的变量,在Inspector里把prefab拉上去,用的时候instantiate 二是Resource.Load,Load以后instantiate 三是AssetBundle.Load,Load以后instantiate 三种方式有细节差异,前两种方式,引用对象texture是在instantiate时加载,而assetBundle.Load会把perfab的全部assets都加载, instantiate时只是生成Clone。所以前两种方式,除非你提前加载相关引用对象,否则第一次instantiate时会包含加载引用类assets的操作,
导致第一次加载的lag。 官方论坛有人说Resources.Load和静态引用是会把所有资源都预先加载的,反复测试的结果,静态引用和Resources.Load也是OnDemand的,用到时才会加载。
亲自测试,第一种静态引用在场景加载的时候就会把所有的资源加载进内存,然后Instantiate的时候再进行克隆+引用
结果场景一加载就闪退。ProFiler
第二种说法,我倒是比较纠结的是Resources.load的时候他是把整个目录下的资源打包做一个缺省AssertBundle引入内存load呢?
还是只是把自己引入内存load呢?
经过测试,应该是只有自己引入内存。
写了一段脚本,测试如下
public class testtest : MonoBehaviour { public float Timer = 0; GameObject[] tmp = new GameObject[Enum.GetValues(typeof(EnemyId)).Length]; void Awake() { StartCoroutine(MyUpdate()); } IEnumerator MyUpdate() { while (true) { int RandomIndex = UnityEngine.Random.Range((int)EnemyId.YangJian, Enum.GetValues(typeof(EnemyId)).Length); string MonsterName = "_Monster/" + ((EnemyId)RandomIndex).ToString(); print(MonsterName); if (tmp[RandomIndex] == null) { tmp[RandomIndex] = Resources.Load(MonsterName) as GameObject; } yield return new WaitForSeconds(5.0f); Debug.Log(Time.time.ToString()); GameObject tmp1 = Instantiate(tmp[RandomIndex]) as GameObject; yield return new WaitForSeconds(2.0f); Debug.Log(Time.time.ToString()); } } }
从上图可以看出,他的内存是一个峰值接着一个峰值,测试结果是对的。
六、项目管理
一些底层的数据比如怪物的属性等等,随着策划的不同而修改,有可能会降低难度,有可能有时候会提高难度,我们可以提供给策划一个excel表格,让策划填写,我们只需要拿着这个excel表格解析出来即可……