2019年5月27日 星期一

在Unity3D使用JSON製作存檔

在Unity3D使用JSON製作存檔

前言

  最近搜尋到Unity3D已經內建支援JSON,想到以前製作存檔是透過PlayerPrefs來儲存相當不好用。想想自己太久沒用Unity3D,該跟上新的製作存檔方式了。

內容

  以前用PlayerPrefs製作存檔不需考慮儲存的位置,因為Unity3D會規劃怎麼儲存,新的做法如下圖
存檔的寫入與讀取

Data是如何轉換成JSON的呢?看以下範例
public class PlayerData
{
  public int intData = 0;
  public float floatData = 3.14159f;
  public PlayerData ptrData = null;//This value will be ignored in json
  public string stringData = "I'm a string";
  public Vector3 vec3Data = Vector3.zero;
  public Matrix4x4 mat44Data = Matrix4x4.identity;
  public List<int> intListData = new List<int>();
  public Dictionary<int, int> intDictData = new Dictionary<int, int>();//This value will be ignored in json
  public Dictionary<string, int> strDictData = new Dictionary<string, int>(); //This value will be ignored in json
}
//
PlayerData data = new PlayerData();
//Convert to json
string jsonStr = JsonUtility.ToJson(data);
//
//Convert to data
PlayerData loadData = JsonUtility.FromJson<PlayerData>(jsonStr);

先把要儲存的資料包成class,不過要注意並不是所有的都可以儲存,會被儲存的資料一定是"public",常見的int、float...等有支援,要注意的是不支援的資料型態,不支援的資料型態是"Reference"與"Dictionary",轉成JSON和轉回資料的部分相當簡單就不多說了。

  接著來看看JSON寫入與讀取的部分,這個部分個人認為使用Binary的方式較為理想(個人比較熟),先看以下範例
using System.IO;
using System.Text; 
//
const string fileName = "playerData.dat";
string filePath = Application.persistentDataPath + "/" + fileName;
string jsonStr = JsonUtility.ToJson(data);
//
// Save
try
{
  File.WriteAllBytes(filePath, Encoding.UTF8.GetBytes(jsonStr));
  Debug.Log("Save OK!");
}
catch (System.Exception e)
{
  Debug.Log("Save failed!");
}
// Load
try
{
  string loadData = Encoding.UTF8.GetString(File.ReadAllBytes(filePath) );
  PlayerData myData = JsonUtility.FromJson<PlayerData>(loadData);
  Debug.Log("Load OK!");
}
catch (System.Exception e)
{
  Debug.Log("Load failed!");
}

寫入的部分要透過"Encodin.UTF8.GetBytes()",而讀取的時候透過"Encodin.UTF8.GetString()"來還原,這個步驟可以讓string的資料可以支援unicode,檔案的開啟與讀取和以前是一樣的。完整的範例如下
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO;
using System.Text;
public class PlayerData
{
  public int intData = 0;
  public float floatData = 3.14159f;
  public PlayerData ptrData = null;//This value will be ignored in json
  public string stringData = "I'm a string.許";
  public Vector3 vec3Data = Vector3.zero;
  public Matrix4x4 mat44Data = Matrix4x4.identity;
  public List<int> intListData = new List<int>();
  public Dictionary<int, int> intDictData = new Dictionary<int, int>();//This value will be ignored in json
  public Dictionary<string, int> strDictData = new Dictionary<string, int>(); //This value will be ignored in json
}
public class SaveLoadTest : MonoBehaviour
{

  // Start is called before the first frame update
  void Awake()
  {
 
  }
  void Start()
  {
    const string fileName = "playerData.dat";
    string filePath = Application.persistentDataPath + "/" + fileName;
    PlayerData data = new PlayerData();
    data.intListData.Add(9527);
    data.intListData.Add(2266);
    data.intDictData.Add(1, 1234);
    data.intDictData.Add(1024, 7788);
    data.strDictData.Add("abc", 123);
    data.strDictData.Add("abc123", 789);
    //
    string jsonStr = JsonUtility.ToJson(data);
    Debug.Log(jsonStr);
    //
    // Save
    try
    {
      File.WriteAllBytes(filePath, Encoding.UTF8.GetBytes(jsonStr));
      Debug.Log("Save OK!");
    }
    catch (System.Exception e)
    {
      Debug.Log("Save failed!");
    }

    // Load
    try
    {
      
      string loadData = Encoding.UTF8.GetString(File.ReadAllBytes(filePath) );
      Debug.Log("loadData:"+loadData);
      PlayerData myData = JsonUtility.FromJson<PlayerData>(loadData);

      Debug.Log("Load OK!");
    }
    catch (System.Exception e)
    {
      Debug.Log("Load failed!");
    }
  }

    // Update is called once per frame
    void Update()
    {
        
    }
}


參考資料

Introduction to Saving and Loading
Unity 遊戲存檔機制淺談,從序列化 (Serialization) 到儲存裝置 (Storage)

2019年5月20日 星期一

計算Perspective camera的頂點位置

計算Perspective camera的頂點位置

前言

  最近需要計算Perspective camera的頂點位置,發現不是很好算,想一下後記起之前在做Shadow map時有算過,為了防止忘記怎麼算,在此做個紀錄。

內容

  在計算之前先來說一下一般表達Perspective camera,用C++來表達的話如下
struct PerspactiveCamera
{
  float fov;
  float aspect;
  float near;
  float far;
};

為了方便說明,所以為每個頂點編號,如下圖
Perspective camera的8個頂點

在計算頂點時必須考慮坐標系,圖中所採用左手坐標系,Z軸代表遠近,X軸為左右,Y軸為上下。頂點"0"、"1"、"2"與"3"所形成的板子稱為"近板",頂點"4"、"5"、"6"與"7"所形成的板子稱為"遠板"。

  先來計算頂點的Z值,遠板的Z值等於"far",近板的Z值等於"near",沒什麼困難,那寬度呢?像是頂點"0"到頂點"1"的長度可以從高度得來,將"0"到頂點"2"的長度乘上"aspect"就可以得到寬度,所重點擺在如何計算高度,看圖中頂點"0"、"2"、"4"與"6"所圍成的板子,在參照下圖
計算遠板與近板的高度

在圖中的紅線代表近板的半高度,綠線則是遠板的半高度,近板的半高度=near*cot(fov/2),遠板的半高度=far*cot(fov/2),半高度值會剛好等於Y值,頂點"0"的Y值就是近板的半高度,頂點"2"的Y值是近板的半高度乘上負1,遠板的Y值依此類推。

   總結的計算如下
struct Point
{
  float x;
  float y;
  float z;
};
struct PerspactiveCamera
{
  float fov;
  float aspect;
  float near;
  float far;
};
//
PerspactiveCamera camera;
//...
//
Point p0,p1,p2,p3,p4,p5,p6,p7;
//
float nearHalfHeight=camera.near*cot(camera.near/2.0f);
float nearHalfWidth=nearHalfHeight*camera.aspect;
float farHalfHeight=camera.far*cot(camera.near/2.0f);
float farHalfWidth=farHalfHeight*camera.aspect;
//
p0.x = -nearHalfWidth;
p0.y =  nearHalfHeight;
p0.z =  near;
//
p1.x =  nearHalfWidth;
p1.y =  nearHalfHeight;
p1.z =  near;
//
p2.x = -nearHalfWidth;
p2.y = -nearHalfHeight;
p2.z =  near;
//
p3.x =  nearHalfWidth;
p3.y = -nearHalfHeight;
p3.z =  near;
//
p4.x = -farHalfWidth;
p4.y =  farHalfHeight;
p4.z =  far;
//
p5.x =  farHalfWidth;
p5.y =  farHalfHeight;
p5.z =  far;
//
p6.x = -farHalfWidth;
p6.y = -farHalfHeight;
p6.z =  far;
//
p7.x =  farHalfWidth;
p7.y = -farHalfHeight;
p7.z =  far;

cot()是三角函數的"Cotangent",標準C++並不提供,如果需要自己實作可以參考Stable Cotangent

參考資料

Stable Cotangent

2019年5月13日 星期一

初探VueLoader

初探VueLoader

前言

  最近利用Vue.js來實作遊戲編輯器的UI遇到了麻煩,Vue.js的Component一定要註冊後才能用,所以程式碼的開頭就會有一大段註冊Component的程式碼,google了一下發現有"loader"可以用,在此把學習的過程做個紀錄。

內容

  "loader"的程式碼可以在http-vue-loader的GitHub下載,接下來製作"vue檔","vue檔"官方有給範例如下
<template>
    <div class="hello">Hello {{who}}</div>
</template>

<script>
module.exports = {
    data: function() {
        return {
            who: 'world'
        }
    }
}
</script>

<style>
.hello {
    background-color: #ffe;
}
</style>

"template"的部分跟以前的一樣,轉成"vue檔"只要剪下貼上即可。"script"就是註冊Component的object,唯一和以前的不同是不再需要設定"template"變數。最後是css的部分,"vue檔"本身可自帶css,和一般的css寫法是一樣的。

  製作好"vue檔"後,接著就可以在程式碼裡使用,官網的範例如下
<!doctype html>
<html lang="en">
  <head>
    <script src="https://unpkg.com/vue"></script>
    <script src="https://unpkg.com/http-vue-loader"></script>
  </head>

  <body>
    <div id="my-app">
      <my-component></my-component>
    </div>

    <script type="text/javascript">
      new Vue({
        el: '#my-app',
        components: {
          'my-component': httpVueLoader('my-component.vue')
        }
      });
    </script>
  </body>
</html>

在header裡先將"loader"的程式碼載入,接著在需要用到Component的Vue新增"Components",
範例被標記為"紅色"的部分,變數的名稱就是Component註冊的名稱,"httpVueLoader"的參數就是讀取的路徑,用起來相當簡單。


參考資料

http-vue-loader的GitHub

2019年5月6日 星期一

分隔線的CSS

分隔線的CSS

前言

  最近在用CSS調整分隔線的顏色,在Chrome還沒問題,但到了Firefox就不太正確,在此把研究結果做個紀錄。

內容

  用CSS提整分隔線的顏色,範例如下
hr
{
  border-top:1px solid rgb(255,0,0);
}

這段範例在Chrome沒問題,但到了Firefox後換看到下圖
在FireFox用範例的CSS調整的分隔線

感覺下方會多一條灰色的線,在Styling <hr> with CSS裡找到分隔線有個"border"屬性,將它調整成"0"後,如下
{
  border:0;
  border-top:1px solid rgb(255,0,0);
}

調整後就可以得到和Chrome一樣的效果,而且這段CSS在Chrome也能正常動作。

  這是我第一次碰到CSS的相容問題,google了一下發現相容的問題還不少,以後有機會再研究研究。

參考資料

W3School_How TO - Style HR
Styling <hr> with CSS