Epoch Development Blog 6 - Float in Physics Engine Issues

 • 

Unity 的物理引擎使用的基础是 float - 无论是 Vector3,Vector2,Quaternion... 等等,都使用 float。而 float 有一个非常大的问题就是精度丢失,特别是在做 Epoch 这种大尺度规模的游戏的时候 —— 尽管我们可以所有东西都缩小到 0.01 倍,但不可避免的是 float 仍然限制 Unity 内同时出现非常大和非常小的物体 —— 例如星球和战斗机。
如果读过 EVE Online 的 Dev Blogs,会发现他们也提到过类似的问题,Titan 舰长 18 千米(更不用提最近新出的 Citadel ),而小型飞船只有 52 米。在用同样的坐标系的情况下,如何保证两种飞船都能在渲染和物理引擎上有同样的表现,本身就是一件非常有挑战性的事情。
回到 Epoch 上,Unity 的物理引擎使用 float,2^23 = 8388608,一共七位,这意味着最多能有 7 位有效数字。如果我们设定 1 unit = 10 meter,那么整个宇宙最多只能有 10^2 - 10^3 km 的大小。而一个超级缩小版星球的半径为 10^2 km —— 星球自身就已经填满了场景,游戏基本上没办法玩了。有关场景大小的讨论还可见 Unity: coordinates and scales

Vector3(100,100,100) 100
Vector3(10000,10000,10000) 100
除此以外,对于物理引擎来说,小数位是绝对不能低于 3 位的,否则就会出现各种 jittering,比如摄像机追踪飞船的时候,飞船本身如果使用 AddRelativeForce() 进行加速,就会有非常剧烈的抖动(如图),并且也不能通过 Rigidbody.Interpolate 或者 Vector3.SmoothDamp 消除,这对于 Motion Blur 是完全不能忍受的。而要保证小数位是不低于 3 位,整数位也就只有 3 - 4 位可以用了 —— 场景大小瞬间砍去一半。 所以,解决方法就是 The Floating Origin Solution —— 即之前提过的 World Streamer 使用的方法:隔一段距离就重置世界坐标到新的 (0,0,0)。这个方法的描述可见 [Unite 2013 - Building a new universe in Kerbal Space Program](https://www.youtube.com/watch?v=mXTxQko-JH0) 以及 [C# - Is a custom coordinate system possible in Unity](http://gamedev.stackexchange.com/questions/110349/is-a-custom-coordinate-system-possible-in-unity)。 [wiki.unity3d.com](http://wiki.unity3d.com/index.php/Floating_Origin) 上有一个现成的示例脚本,其中的操作是在 LateUpdate() 中完成的。这个脚本目前来说唯一的问题就是会影响系统自带的 Trail Render,因为自带的 Trail Render 并没有整体移动的功能,也没有办法让 Trail Mesh 作为某个 Transform 的 child,所以解决办法只能是等学校补考过后自己写一个 Trail Render 好了。

Unity & Doxygen

 • 

三个脚本。
Doxygen 生成的文件位置在与 Assets 同级的 Documentation 文件夹内,Doxyfile 与 GenDoc.command 同级,都在 Assets/FinGameWorks/Scripts/Documentation/ 内


JZDoxygenManager.cs

using UnityEngine;
using UnityEditor;
using System.Collections;
using System.Diagnostics;

namespace FinGameWorks.Editor
{
	public class JZDoxygenManager : EditorWindow{
		
		[MenuItem ("Window/FinGameWorks/Documentation/Gen Doc")]
		static void Init () 
		{
			RegenDoc();
		}
		
		public static void RegenDoc ()
		{
			Process proc = new System.Diagnostics.Process ();
			proc.StartInfo.FileName = "open";
			proc.StartInfo.Arguments = "-b com.apple.terminal "+ Application.dataPath +  "/FinGameWorks/Scripts/Documentation/GenDoc.command";
			proc.Start();
		}
	}
}

GenDoc.command

cd "$(dirname "$([ -L $0 ] && readlink -f $0 || echo $0)")"
doxygen Doxyfile
echo “Generation Done”
sleep 1
killall Terminal

JZ_DocumentationWindow.cs

using UnityEngine;
using System.Reflection;
using System.IO;

namespace FinGameWorks.Documentation
{
	#if UNITY_EDITOR
	using UnityEditor;
	/// <summary>
	/// 文档浏览器窗口
	/// </summary>
	[InitializeOnLoad]
	public class JZ_DocumentationWindow : ScriptableObject
	{
		static BindingFlags Flags = BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy;

		[MenuItem("Window/FinGameWorks/Documentation/Show Doc")]
		static void Open()
		{

		#if (UNITY_5_3 || UNITY_5_2 || UNITY_5_1 || UNITY_5_0)

		 var type = Types.GetType ("UnityEditor.Web.WebViewEditorWindow", "UnityEditor.dll");
        var methodInfo = type.GetMethod ("Create", Flags);
        methodInfo = methodInfo.MakeGenericMethod (typeof(JZ_DocumentationWindow));

		#elif UNITY_5_4

		var type = Types.GetType ("UnityEditor.Web.WebViewEditorWindowTabs", "UnityEditor.dll");
        var methodInfo = type.GetMethod ("Create", Flags);
        methodInfo = methodInfo.MakeGenericMethod (type);

		#endif

			string path = Directory.GetParent(Application.dataPath).FullName + "/Documentation/html/index.html";
			methodInfo.Invoke(null, new object[]
			{
				"Documentation",
				path,
				200, 530, 800, 600
			});
		}
	}
	#endif
}

Unity Inspector Custom Tabbar

 • 

效果类似于 Terrain Inspector 的顶层 Tabbar,不过按钮之间有间距,这个如果需要完全一样也可以改 GUIStyle 实现。

public class JZSpaceShipControllerEditor : Editor
	{
		/// <summary>
		/// 飞船控制器编辑器 Tab 类别枚举
		/// </summary>
		enum JZSpaceShipControllerEditorTab
		{
			General,
			Control,
			Weapon
		};

		/// <summary>
		/// Tab Button Inactive State 样式
		/// </summary>
		private static GUIStyle ToggleButtonStyleNormal = null;
		/// <summary>
		/// Tab Button Active State 样式
		/// </summary>
		private static GUIStyle ToggleButtonStyleToggled = null;
		/// <summary>
		/// 飞船控制器编辑器当前选中的 Tab
		/// </summary>
		private JZSpaceShipControllerEditorTab currentTab;

		public override void OnInspectorGUI()
		{
			JZSpaceShipController spaceShipController =
				(JZSpaceShipController) target;

			if (ToggleButtonStyleNormal == null || ToggleButtonStyleToggled == null)
			{
				ToggleButtonStyleNormal = "Button";
				ToggleButtonStyleToggled = new GUIStyle(ToggleButtonStyleNormal);
				ToggleButtonStyleToggled.normal.background = ToggleButtonStyleToggled.active.background;
			}

			GUILayout.BeginHorizontal();
			foreach (JZSpaceShipControllerEditorTab tab in Enum.GetValues(typeof(JZSpaceShipControllerEditorTab)))
			{
				if (GUILayout.Button(tab.ToString(),
					currentTab == tab ? ToggleButtonStyleToggled : ToggleButtonStyleNormal))
				{
					currentTab = tab;
				}
			}
			GUILayout.EndHorizontal();

			switch (currentTab)
			{
				case JZSpaceShipControllerEditorTab.General:
				{
					EditorGUILayout.HelpBox("Space Ship Controller",MessageType.Info);
				}
					break;
				case JZSpaceShipControllerEditorTab.Control:
				{
				}
					break;
				case JZSpaceShipControllerEditorTab.Weapon:
				{
				}
					break;
			}
		}
	}

2016.8.31

 • 

在学校上课有三天了,还没看到大一新生的影子。新的学期自己已经大三了。按理说一般大学生这个时候也该确定自己要去做什么了,不过我是再也不想去谈这个事情了 —— 谈到这个总是有和父母不断的毫无意义和结果的争吵,我已经够烦的了。
总之,在我和父母这么长时间的拉扯之后我逐渐明白了,要想达到自己理想的地步,就只能通过吵架来解决问题。听起来很不正常,但是在父母不把自己的观点当一回事的情况下,自己取得的成果总是会被轻视(比如仍然觉得我走了弯路),所以只能通过吵架来取得父母的让步,即使后果是父母伤心,但这是必要的。
人都是趋利的,而获得对自信也算是对自己利好的一种,特别是对于本来就不甚自信的人来说。所以,考虑到在学校参加所谓的软件比赛两次都没有什么好结果,然而去参加黑客松却能有几连冠的时候,我会做出什么决定似乎也没有什么需要解释的地方。

2016.8.22

 • 

在推上吐槽了下 微信 for Mac 2.0,竟然在一天世界的会员通信中被引述作为讨论微信的开头。不鳥萬如一在讨论的最后如是说:“执意要用户使用内建浏览器最终一定是为了实现更好的控制,但如果介意这个,完全不用微信方为正途。”


Star Citizen: 2016 Gamescom Live Alpha 3.0 Demo
Star Citizen 这个视频展示的估计是太空游戏在某种层面的顶尖水平了:星球是真正 1:1 scale 的,而不是 No Man's Sky 里行走面积只有一个地级市那么大的星球(i-Novae Studios 的 Battlescape 其实也能达到类似的效果,而且是最早达到类似效果的
1
,但是毕竟资源不足,开发进度比星际公民慢了很多)。而做到这个,想必在物理引擎和渲染引擎的数字精度上花了不少力气。至于 Unity 中能不能实现类似的效果,我在 Asset Store 里面见过 World Streamer,大概原理是隔一段距离就重置世界坐标到新的 (0,0,0) 并且 Unload 掉无用的场景,这样能避免物理引擎在较大 unit 上出现非常奇怪的精度丢失,但问题是,这个插件仍然是需要在编辑器内预先做好场景然后切割为很多用于 load / unload 的小部分放置于 Unity Build Scenes 里,而不是实时切割。


昨天刚回到家,估计在家呆几天,然后回学校补考。至于什么时候回公司,要看和辅导员,和院里面的老师怎么聊了,然而不幸的是我及其不擅长和大学老师聊天。

  1. 2010 年 i-Novae Studios 就发布过无缝登录的巨大星球登录视频

Epoch Dev Blog 5

 • 

上一次 Epoch Dev Blog 还是三月,中间因为 Unity 在超大 unit 尺度上频繁崩溃的原因几乎就 AFK 了。不过最近 Unity 崩溃的情况有所转好,而且在北京的这段时间几乎每个人看到我的这个 Epoch 原型都在问我为什么不继续开发。但我并不准备在二月的原型上继续开发了,因为当时的代码效率非常低,结构也很乱,现在再看几乎是恨不得重写一遍。
所以我的确重写了 —— 而且我打算分两步走,第一步先基于 Epoch 的核心代码重写一个新的 Epoch Core,包括过程生成的大部分代码,然后生成一个类似于 Mountain 的小工程。然后再基于新的 Epoch Core 填充各类素材,设计故事,等等等。
Teaser Time
最近和 Epoch 比较相关的的事情就是 No Man's Sky 的发售了。差评较多,主要还是集中在核心玩法和优化上:一个具有完整 FPS 视角的太空游戏,大部分时间的操作和 Minecraft 一样,却并没有像 Minecraft 一样直观的反馈,并且不断崩溃。这和很多玩家的想象偏差(也和 Hello Games 在游戏展上演示的效果偏差)很多。过程生成,包括贴图,地形,音效等都只能解决素材问题,但还是不能解决玩法问题。Elite Dangerous 的思路或许更为正常些:它一直宣传自己是一个纯正的太空战斗游戏,而不是什么第一人称射击游戏。

使用 Processing-Android自己实现 METER 动态壁纸

 • 

Recreate METER Wallpapaer using Processing-Android!
关于 METER : METER On Androidexperiments
想要自己实现一个 WIFI 信号的动态壁纸,于是尝试了下。提示:需要 Processing-Android 4.0 Pre-Release

步骤一:画背景及 WIFI 信号背景三角形

Reference :
background() | triangle()

void setup() 
{
  fullScreen(P3D);//当然 P2D 应该也行的样子。
}

void draw() 
{
  background(12, 39, 43);// 背景色
  beginShape(TRIANGLES);
  fill(0,94,83);// WIFI 三角背景色
  vertex(width / 6 * 1, height / 5 * 1);
  vertex(width / 2, height / 5 * 3);
  vertex(width *5 / 6, height / 5 * 1);
  endShape();
}

效果:

步骤二:画 WIFI SSID 字段

先在 Processing - Android - Sketch Permissions 里勾选 android.permission.ACCESS_WIFI_STATE,否则读取 WIFI 信息会闪退。获取 Context 参见 processing-android/issues/227 里的 Commit,这里是用的是 surface.getComponent();

import android.net.wifi.*;
import android.content.*;
import android.app.*;
import processing.core.*;
import android.os.Bundle;
import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorManager;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;

String wifiName;
android.content.Context context;

void setup() 
{
  fullScreen(P3D);
}

void draw() 
{
  background(12, 39, 43);
  beginShape(TRIANGLES);
  fill(0,94,83);
  vertex(width / 6 * 1, height / 5 * 1);
  vertex(width / 2, height / 5 * 1 + width / 3 * 2);
  vertex(width *5 / 6, height / 5 * 1);
  endShape();
  
  textSize(width / 20); // WIFI SSID 字体大小
  textAlign(CENTER, CENTER);
  text("WIFI : "+wifiName, width / 2, height / 5 * 1 + width / 3 * 2 + height / 20);
  fill(0, 102, 153);
}
public void onResume() {
  super.onResume();  
  context = (Context) surface.getComponent();
  WifiManager wifiManager = (WifiManager) context.getSystemService (Context.WIFI_SERVICE);
  WifiInfo info = wifiManager.getConnectionInfo ();
  wifiName = info.getSSID(); // 获取 SSID
}
public void onPause() {
  super.onPause();
}

步骤三:画 WIFI 强度

这里还有一个调整,就是获取信号强度和 SSID 都不在 onResume() 做了,因为 onResume() 一般是后台转前台才触发,而我们要保证在 Launcher 里面能动态刷新,所以设立一个 Timer 来每十秒钟获取好了。
截图里面的三角形是全填满的,公司信号太好,我懒得找一个没信号的地方测试了,反正公式是对的就行啦。

import android.net.wifi.*;
import android.content.*;
import android.app.*;
import processing.core.*;
import android.os.Bundle;
import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorManager;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import java.util.*;

String wifiName;
int wifiLevel; // 0 - 10
android.content.Context context;
boolean canRefreshWifi;

float Point1X,Point2X,Point3X;
float Point1Y,Point2Y,Point3Y;

void setup() 
{
  fullScreen(P3D);
  
    Timer t = new Timer(); // 每 10 秒 Run 一次。
    t.scheduleAtFixedRate(new TimerTask() {
        @Override
        public void run() {
            if (canRefreshWifi)
            {
              context = (Context) surface.getComponent();
  WifiManager wifiManager = (WifiManager) context.getSystemService (Context.WIFI_SERVICE);
  WifiInfo info = wifiManager.getConnectionInfo ();
  wifiName = info.getSSID();
  wifiLevel = WifiManager.calculateSignalLevel(info.getRssi(), 11); // 信号强度
            }
        }
    
    },0,10000);
}

void draw() 
{
  background(12, 39, 43);
  beginShape(TRIANGLES); // 背景三角形
  fill(0,94,83);
  vertex(Point1X, Point1Y);
  vertex(Point2X, Point2Y);
  vertex(Point3X, Point3Y);
  endShape();
  
  beginShape(TRIANGLES); // 前景三角形
  fill(37,206,182);
  vertex(Point2X + (Point1X - Point2X) * wifiLevel / 10, Point2Y + (Point1Y - Point2Y) * wifiLevel / 10);
  vertex(Point2X, Point2Y);
  vertex(Point2X + (Point3X - Point2X) * wifiLevel / 10, Point2Y + (Point3Y - Point2Y) * wifiLevel / 10);
  endShape();
  
  textSize(width / 20);
  textAlign(CENTER, CENTER);
  text("WIFI : "+wifiName, width / 2, height / 5 * 1 + width / 3 * 2 + height / 20);
  fill(0, 102, 153);
}
public void onResume() {
  super.onResume();  
  canRefreshWifi = true; // 回到前台,可以继续刷新 WIFI 信息。
  
  Point1X = width / 6;
  Point2X = width / 2;
  Point3X = width / 6 * 5;
  
  Point1Y = height / 5;
  Point2Y = height / 5 * 1 + width / 3 * 2;
  Point3Y = height / 5;
}
public void onPause() {
  super.onPause();
  canRefreshWifi = false;
}

步骤四:实现水平仪效果

加上地理位置的权限,反正就是 ACCESS 开头中间带 LOCATION 的两个,我也忘了具体名字了。
代码略长了,于是放在了 Gist 上。后面写的有些乱,效率上非常差,不过作为示范应该还是能看的。如图。

到这里,基本上实现了一个功能上和 METER 差不多的动态壁纸,可以显示 SSID,显示信号强弱,自带水平仪效果。

2016.8.14

 • 

都 14 号了,过的真快。

期间父母来北京看了我一次。聊了些日常的话题,没吵。这样我已经很满足了,所以没有再扯些其他的,不过还是和母上说了些更远的期望。
比如最好,不要认为现在就可以准备养老了,要能在这个岁数继续学习下去,见识更多的事情。否则,我感觉即使在大三要不要呆北京实习这个事情上我和父母达成一致了,以后还会有更多的分歧。

那么大三到底要不要呆北京呢,这个还要等九月份回去和辅导员商量才能有结果。其实辅导员对我不错,只不过我一直回避直接的接触,但拖不是个办法,事情总是要说的罢。

父母来的那个周末我还想起来一个事情。学校有一个同学,如果和他说要去做一些比较出头的事情,他语气就会变得非常拖拉,肢体语言上也显得非常扭捏和退缩,但他自己永远也不会意识到。后来我想肯定有时候自己也变得这么不自信,明显得外人可以感受到但自己却不自知,真是一件非常糟糕的事情。所以事情还是多勇于尝试为好,至少到后面就不会再有问题了。

和 Cee 一起买了同款耳机,朱砂红的索尼无线耳罩耳机,至于型号就不写了,因为我对耳机真的也没有做过多的研究,没办法详细讲解一番。

马老师请我们(Cee,我,Lighters)吃了火锅。记得四月份临走的时候,和响哥马老师吃饭也是火锅。然后下午参观了多点科技,以及见到了有才

提到设计师们,我最近有个感想是,之前我还在简历上写自己是设计爱好者来着,现在越来越觉得自己什么都不是。设计不是一个靠灵感的工作,或者说,设计很多时候并不需要拼灵感,而是在遵循正常的规律。而必须需要灵感才产出内容的设计师,能做的很好,也能做的非常差,完全看设计师本人的把握。想到这里就觉得自己还是不要瞎掺和了。

的确有很多程序员想学习设计,有很多设计师想学习代码,毕竟,同时掌握开发和设计(我指用户界面设计 / 媒体交互设计)在两个职业的人来看都是一件很酷的事情。有这种想法和热情的人,我两面都见了不少,但我觉得,要真的实现的确是太难了,毕竟这不是说程序员拿个 Dribbble Invite 或者设计师写个 Swift Hello World 就可以了的事情。

C4D 渲染像素风小技巧

 • 

今天才学到一个。
输出 1920 * 1080 的视频,每个像素大小 10 * 10,于是 Renderer Settings 设置实际渲染为 192 * 108:

渲染到 Picture Viewer 上,序列帧右键 Save as

(近似)无损拉伸到 1080P,非常适合渲染像素风。

2016.8.4

 • 

多图。

最近一直忙着公司的东西了,Cetacea for Mac 没有继续开发,不过完成度基本够我放出来一些图。

这个就是 Cetacea 的主界面,夜间模式。如果想要改主题 / 日夜自动切换模式,可以进设置:

点击主题,就会出现一个详细的颜色表:

Editor 的两个细节:当前编辑器行号加粗 和 URL 点击预览

目前能放出来的大致就这些。左侧 Bar 应该最后会改成和 Ulysses 一样,而不是图上的文件查看器。Preview 界面基本上就是实时预览该有的样子,目前基本没动工。


既然都发了这么多图,再发些最近手机拍的照片好了。


Nexus 6,低价入的。挺好用,搞得我都想学习 Android 开发了。(实际上我现在已经基本在写了)


楼下胜博殿外面的巨大毛绒狗,也是一个 Ingress Portal,名为“狗叼鸭”还是什么的


某天到早晨才出公司,准备回住处睡觉。


写字楼往上看。说不清这是什么结构。


猫,挺粘人。