It's a plugin that allows you to easily load CMS data into ScriptableObject
.
This plugin is built on the concept of being "Easy to input and delivering top-notch performance".
When it comes to input, we think of CMS. CMS is filled with knowledge on how to input data easily and without stress. Also, it gives the ease of being able to be updated from anywhere.
ScriptableObject
is a data format that is optimized for handling in Unity. It has excellent performance.
However, these two may seem unrelated at first glance. But it is CMSuniVortex that connects CMS and ScriptableObject
. This is not a mere plugin, but a solution born out of pursuing efficiency and performance.
You can specify how to refer to the data you have output.
- Direct reference
- Referenced via Addressables
Unity 2021.3.x or higher
Please add the URL to "Window > Package Manager > Add package from git URL...".
URL: https://github.com/IShix-g/CMSuniVortex.git?path=Packages/CMSuniVortex
Right click on the Project and select "Create > CMSuniVortex > create CuvImporter" to create a CuvImporter
.
Click the "Script Generator" button on the generated CuvImporter
.
Enter the necessary information to generate the code. In this case, we generate the code for Cockpit.
explanation | e.g. | |
---|---|---|
Full Class Name | Specify the class name. Namespace can also be specified. | namespace.ClassName |
Build Path | Specify the path of the directory to generate the code. | Assets/Models/ |
After generating, return to CuvImporter and enter the necessary information. Specify the script generated earlier as the client. This time, we selected CatDetailsCockpitCuvClient
for direct reference. If using Addressables, select the AddressableClient
above it.
Naming rule for output Client: "Full class name specified when generating code" + "CMS name" + "Output name" + "CuvClient"
explanation | e.g. | |
---|---|---|
Build Path | Specify the directory where the data will be output. | Assets/Models/ |
Languages | Specify the language, even if not used, at least one needs to be selected. | English |
Client | Specify any client for direct reference or Addressables, etc.. | Test.ClassNameCockpitCuvClient |
Output | Decide how to refer to the data output by the client. | Test.ClassNameCockpitCuvOutput |
explanation | e.g. | |
---|---|---|
Base Url | URL where Cockpit is installed | https://xxx.xxx.com/cockpit/ |
Api Key | Api Key obtainable from the Cockpit admin page | English |
Model Name | Model name set on Cockpit's admin page | Model |
Actual tests using Cockpit CMS are possible. Please use the following.
value | |
---|---|
Base Url | https://devx.myonick.biz/cockpit/ |
Api Key | API-a92fac21986ac045e143f07c27c60e09f19ae856 |
Model Name | Model |
Although the permission is read-only, you can actually log in and view the admin page.
value | |
---|---|
URL | https://devx.myonick.biz/cockpit/ |
ID | guest |
PW | guest |
- Please use in moderation.
- Do not access too frequently.
- Do not perform consecutive imports.
- Although advertisements are displayed because I use a free rental server, I am not involved at all.
- Please note that we may stop without notice if we find inappropriate access.
After input, click Import, and the data is generated in the specified directory.
Decide how to refer to the imported data. This time, we select CatDetailsCockpitCuvOutput
for direct reference.
After selection, click on Output to generate it.
Data can be retrieved using GetList()
from the generated CatDetailsCockpitCuvReference
. If you use the prepared CuvComponent
, you can retrieve it as follows.
The instance of Reference and the Key set on the inspector are passed, so you use TryGetByKey
to retrieve it.
using UnityEngine;
using UnityEngine.UI;
using CMSuniVortex.Compornents;
public sealed class TestText : CuvComponent<CatDetailsCockpitCuvReference>
{
[SerializeField] Text _text;
protected override void OnChangeLanguage(CatDetailsCockpitCuvReference reference, string key)
{
if (reference.GetList().TryGetByKey(key, out var model))
{
_text.text = model.Text;
}
}
}
※ CuvAsyncComponent
is used for Addressables.
For details on how to set up, please see here.
You can check representative classes that constitute the plugin here.
What prompted me to develop this plugin was performance testing. There are roughly three methods to download data and display it.
Good performance with no need for deserialization or data conversion by using ScriptableObject
or Sprite
Since it needs to be exported by Unity, a programmer is required. Or a significant conversion system needs to be established.
Test Code
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.Profiling;
public sealed class AddressableTest : MonoBehaviour
{
[SerializeField] Image _image;
[SerializeField] Text _text;
[SerializeField] Button _loadButton;
[SerializeField] Button _unloadButton;
AsyncOperationHandle<AddressableData> _handle;
void Start()
{
_loadButton.onClick.AddListener(OnLoadButtonClicked);
_unloadButton.onClick.AddListener(OnUnloadButtonClicked);
_loadButton.interactable = true;
_unloadButton.interactable = false;
}
void OnDestroy() => Unload();
async void OnLoadButtonClicked()
{
_loadButton.interactable = false;
_unloadButton.interactable = true;
Profiler.BeginSample("AddressableTestProfile1");
_handle = Addressables.LoadAssetAsync<AddressableData>("AddressableData");
Profiler.EndSample();
await _handle.Task;
Profiler.BeginSample("AddressableTestProfile2");
var obj = _handle.Result;
_image.sprite = obj.Image;
_text.text = obj.GetText();
Profiler.EndSample();
}
void OnUnloadButtonClicked()
{
Unload();
_loadButton.interactable = true;
_unloadButton.interactable = false;
}
void Unload()
{
if (_image != default)
{
_image.sprite = default;
}
if (_text != default)
{
_text.text = default;
}
if (_handle.IsValid())
{
Addressables.Release(_handle);
}
}
}
[CreateAssetMenu(fileName = "AddressableData", menuName = "ScriptableObject/AddressableData", order = 0)]
public sealed class AddressableData : ScriptableObject
{
public int ID;
public string Title;
public string Contents;
public Sprite Image;
public string GetText() => "ID:" + ID + "\nTitle:" + Title + "\nContents:" + Contents;
}
Webview from Cross Platform Essential Kit
- Can be used for both WEB pages and applications.
- The layout can be free even after release.
- Concerned about the amount of memory used.
Test Code
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Profiling;
using VoxelBusters.CoreLibrary;
using VoxelBusters.EssentialKit;
public sealed class WebViewTest : MonoBehaviour
{
const string url = "https://xxx.xxxx.com/webview/";
[SerializeField] Button _openButton;
[SerializeField] Button _closeButton;
WebView _webView;
void Start()
{
_openButton.onClick.AddListener(ClickOpenButton);
_closeButton.onClick.AddListener(ClickCloseButton);
_openButton.interactable = true;
_closeButton.interactable = false;
}
void OnEnable()
{
WebView.OnShow += OnWebViewShow;
WebView.OnHide += OnWebViewHide;
}
void OnDisable()
{
WebView.OnShow -= OnWebViewShow;
WebView.OnHide -= OnWebViewHide;
}
void ClickOpenButton()
{
_openButton.interactable = false;
Profiler.BeginSample("WebViewTestProfile");
_webView = WebView.CreateInstance();
_webView.SetNormalizedFrame(new Rect(0.1f, 0.2f, 0.8f, 0.6f));
_webView.LoadURL(URLString.URLWithPath(url));
_webView.Show();
Profiler.EndSample();
}
void ClickCloseButton() => _webView.Hide();
void OnWebViewShow(WebView view) => _closeButton.interactable = true;
void OnWebViewHide(WebView view)
{
_openButton.interactable = true;
_closeButton.interactable = false;
}
}
Webページ
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>Test</title>
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<meta name="format-detection" content="telephone=no,email=no,address=no">
<style type="text/css">
img{
max-width: 100%;
}
</style>
</head>
<body>
<div id="myData">
<h2 id="title"></h2>
<p id="contents"></p>
<img id="image" src="" alt="Image">
</div>
<script src="https://code.jquery.com/jquery-1.12.4.min.js"></script>
<script>
$.ajax({
url: 'getModel.php',
dataType: 'json',
success: function(data) {
$('#title').text(data.Title);
$('#contents').text(data.Contents);
$('#image').attr('src', data.Image);
},
error: function (request, status, error) {
console.log("Error: Could not fetch data");
}
});
</script>
</body>
</html>
データを取得するAPI
<?php
class Model {
public $Id;
public $Title;
public $Contents;
public $Image;
}
mb_language("uni");
mb_internal_encoding("UTF-8");
header('Content-type: application/json');
$model = new Model();
$model->Id = 2222;
$model->Title = '猫 ねこ';
$model->Contents = '猫は、古代のミアキスと言う豹のような大きな動物が起源と言われています。 今から4000~5000年前にエジプトから発生し、住み良い環境を求め分化して中東に行きました。';
$model->Image = 'https://xxx.xxxx.com/webview/cat.jpg';
echo json_encode( $model );
Display by converting JSON obtained from the server using UnityWebRequest.
- Can be used for WEB and apps.
- Apart from initialization, it seems lighter than WebView.
- There is a concern about the initialization cost if there are many images (only one image in the test).
- If you don't cache data, you need to provide your own caching mechanism.
Test Code
using System;
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Networking;
using UnityEngine.Profiling;
public sealed class JsonTest : MonoBehaviour
{
const string apiUrl = "https://xxx.xxxx.com/webview/getModel.php";
[SerializeField] Image _image;
[SerializeField] Text _text;
[SerializeField] Button _loadButton;
[SerializeField] Button _unloadButton;
[Serializable]
sealed class Model
{
public int ID;
public string Title;
public string Contents;
public string Image;
public string GetText() => "ID:" + ID + "\nTitle:" + Title + "\nContents:" + Contents;
}
void Start()
{
_loadButton.onClick.AddListener(OnLoadButtonClicked);
_unloadButton.onClick.AddListener(OnUnloadButtonClicked);
_loadButton.interactable = true;
_unloadButton.interactable = false;
}
void OnDestroy() => Unload();
void OnLoadButtonClicked()
{
_loadButton.interactable = false;
_unloadButton.interactable = false;
StartCoroutine(LoadCo((model, sprite) =>
{
_text.text = model.GetText();
_image.sprite = sprite;
Profiler.EndSample();
_unloadButton.interactable = true;
}));
}
IEnumerator LoadCo(Action<Model, Sprite> onSuccess)
{
Profiler.BeginSample("JsonTestProfile1");
using var request = UnityWebRequest.Get(apiUrl);
Profiler.EndSample();
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
Profiler.BeginSample("JsonTestProfile2");
var model = JsonUtility.FromJson<Model>(request.downloadHandler.text);
using var imgRequest = UnityWebRequestTexture.GetTexture(model.Image);
Profiler.EndSample();
yield return imgRequest.SendWebRequest();
if (imgRequest.result == UnityWebRequest.Result.Success)
{
Profiler.BeginSample("JsonTestProfile3");
var texture = ((DownloadHandlerTexture)imgRequest.downloadHandler).texture;
var sprite = Sprite.Create(
texture,
new Rect(0, 0, texture.width, texture.height),
new Vector2(0.5f, 0.5f));
onSuccess?.Invoke(model, sprite);
}
else
{
Debug.LogError(imgRequest.error);
}
}
else
{
Debug.LogError(request.error);
}
}
void OnUnloadButtonClicked()
{
Unload();
_loadButton.interactable = true;
_unloadButton.interactable = false;
}
void Unload()
{
if (_image != default
&& _image.sprite != default)
{
var tex = _image.sprite.texture;
_image.sprite = null;
DestroyImmediate(tex);
Resources.UnloadUnusedAssets();
}
if (_text != default)
{
_text.text = default;
}
}
}
From these tests, we learned that:
- Addressable performs the best.
- WebView uses significant memory on Android. It might not be possible to fully release all memory.
- Json has a significant initialization cost when there are many images.
From these results, I wanted to use Addressable, which has the best performance, but also allows for easy updates from the CMS, so I developed this plugin.
GC Alloc | Time | Size | |
---|---|---|---|
Addressables | 3.2KB | 0.24ms | 1.1MB |
WebView | 22.9KB | 0.52ms | 2MB |
Json | 15KB | 3.75ms | 2.3MB |
GC Alloc | Time | Size | |
---|---|---|---|
Addressables | 3.1KB | 0.24ms | 9MB |
WebView | 31.8KB | 0.56ms | 70MB |
Json | 4.3KB | 1.18ms | 9.7MB |
Currently, the system only supports up to the generation of ScriptableObject
. However, we are planning to enhance it to handle the locally generated objects and to build Addressable to send to a server. We would also like to increase support for CMS. If you are interested, we appreciate your cooperation.