Django Backend
まず Django のプロジェクトを作る.
django-admin startproject DeepCubeServer
Django に必要な設定を行い,tests
という app を作る.
python3 manage.py startapp tests
Django のフォルダーにおいて,前回作ったクラスを DeepCube
というフォルダーに移動する./DeepCube/__init__.py
を忘れずに作る.
前回のクラスを継承して,新しく CubeUnity
クラスを作成し,Json で渡せる形の辞書を作りたい.Unity で利用するので,最も理想の形は,各 Cubelet の各面の色の説明辞書を渡すことだと思われる.これは前回ちゃんと定義したクラスから簡単に作れる.(コメントを除くわずか 12 行)
class CubeUnity(Cube): """ Cube class use for unity. """ def get_description(self) -> dict[str, dict[int, int]]: """ Target format 20 x 3 dictionary. :return: { SLOT: { FACE_CHAR: COLOR_CHAR, ... }, ... } e.g. { 'UFR': { 'U': 'W', 'F': 'R', 'R': 'G' }, ... } """ # Center description = {face: {face: COLORS_[code]} for face, code in FACES.items()} for i in range(20): slot = CUBELETS_[i] colors = [COLORS_[FACES[j]] for j in CUBELETS_[self.states[i][0]]] phase = self.states[i][1] divisor = 3 if i < 8 else 2 faces = [slot[(j + phase) % divisor] for j in range(divisor)] print(slot, faces, colors) description[slot] = {j: k for j, k in zip(faces, colors)} return description
最後に,/tests/views.py
で Unity で呼び出すための API を作る.
def test_cube_class(request): """ A restful API to test if the `move` method of `Cube` class work correctly. GET parameters: actions string e.g. "UFuf" return: Json Dictionary. See `CubeUnity`. """ actions = request.GET.get('actions', '') cube = CubeUnity() try: for a in actions: cube.move(a) except ValueError: return JsonResponse({}, status=403) return JsonResponse(cube.get_description(), status=200)
Unity
Unity では,Script のみでキューブを作成する.空の GameObject を作り,GameMaster
と名付ける.C# の Script TS1GM.cs
を作成し,GM にくっつける.
まずは必要な定数と変数を定義する.
// Faces private readonly char[] faces = { 'U', 'D', 'F', 'B', 'L', 'R' }; // Colors private readonly Dictionary<char, Color> colorDict = new() { {'K', new Color(0f, 0f, 0f)}, {'B', new Color(0f, 70f / 255f, 173f / 255f)}, {'G', new Color(0f, 155f / 255f, 72f / 255f)}, {'O', new Color(255f / 255f, 88f / 255f, 0f)}, {'R', new Color(183f / 255f, 18f / 255f, 52f / 255f)}, {'W', new Color(1f, 1f, 1f)}, {'Y', new Color(255f / 255f, 213f / 255f, 0f)} }; // Position and Scale of stickers. private readonly Dictionary<char, Vector3> stickerPositionDict = new() { {'U', new Vector3(0f, 0.49f, 0f)}, {'D', new Vector3(0f, -0.49f, 0f)}, {'F', new Vector3( 0.49f, 0f, 0f)}, {'B', new Vector3(-0.49f, 0f, 0f)}, {'L', new Vector3(0f, 0f, -0.49f)}, {'R', new Vector3(0f, 0f, 0.49f)}, }; private readonly Dictionary<char, Vector3> stickerScaleDict = new() { {'U', new Vector3(0.85f, 0.04f, 0.85f)}, {'D', new Vector3(0.85f, 0.04f, 0.85f)}, {'F', new Vector3(0.04f, 0.85f, 0.85f)}, {'B', new Vector3(0.04f, 0.85f, 0.85f)}, {'L', new Vector3(0.85f, 0.85f, 0.04f)}, {'R', new Vector3(0.85f, 0.85f, 0.04f)}, }; // Position of cubelets. private readonly Dictionary<char, Vector3> cubeletPositionDict = new() { {'U', new Vector3(0f, 1f, 0f)}, {'D', new Vector3(0f, -1f, 0f)}, {'F', new Vector3( 1f, 0f, 0f)}, {'B', new Vector3(-1f, 0f, 0f)}, {'L', new Vector3(0f, 0f, -1f)}, {'R', new Vector3(0f, 0f, 1f)}, }; // URL private const string URL = "http://127.0.0.1:8000/tests/cube_class/"; // Cube Object private GameObject cube; private string currentActions = "";
次に,小さい Cubelet 及びその上の Sticker を作るための method を作る.
private void CreateCubelet(string cubeletName, Vector3 position, Dictionary<char, char> face2color, GameObject parentObj) { // Initialize. GameObject cubelet = GameObject.CreatePrimitive(PrimitiveType.Cube); cubelet.name = cubeletName; cubelet.GetComponent<MeshRenderer>().material.color = colorDict['K']; // Set up stickers. foreach (char face in faces) { GameObject sticker = GameObject.CreatePrimitive(PrimitiveType.Cube); sticker.name = face.ToString(); // Set position, scale. sticker.transform.position = stickerPositionDict[face]; sticker.transform.localScale = stickerScaleDict[face]; sticker.GetComponent<MeshRenderer>().material.color = face2color.TryGetValue(face, out char color) ? colorDict[color] : colorDict['K']; // Grouping. sticker.transform.SetParent(cubelet.transform); } cubelet.transform.position = position; cubelet.transform.SetParent(parentObj.transform); }
- cubeletName: URF みたいな名前,実質な用途はないが,もし Debug が必要になったら使えるかもしれない
- position: cubelet が cube に関する相対位置.
- face2color: 先 Django で定義した辞書.サーバーから Json の形で渡し,
_UpdateCube
で C# の辞書にしてここに渡す. - parentObj: どの Cube の cubelet かを指定する.
次に,サーバーに description を請求して,その中身を用いて先定義した method を呼び出す.
private IEnumerator _UpdateCube(string actions) { currentActions += actions; // Request Json response. string uri = string.IsNullOrEmpty(currentActions) ? URL : $"{URL}?actions={currentActions}"; print(uri); using var webRequest = UnityWebRequest.Get(uri); yield return webRequest.SendWebRequest(); string[] pages = uri.Split('/'); int page = pages.Length - 1; switch (webRequest.result) { case UnityWebRequest.Result.ConnectionError: case UnityWebRequest.Result.DataProcessingError: case UnityWebRequest.Result.ProtocolError: Debug.LogError($"{pages[page]}: Error: {webRequest.error}"); break; case UnityWebRequest.Result.Success: // Convert Json to Dictionary. var jsonDict = JsonConvert.DeserializeObject<Dictionary<string, Dictionary<char, char>>>(webRequest.downloadHandler.text); // Reinitialize cube. Destroy(cube); cube = new GameObject("Cube"); // Set up cubelets. foreach (var (slot, face2color) in jsonDict) { Vector3 position = slot.Aggregate(Vector3.zero, (_position, face) => _position + cubeletPositionDict[face]); CreateCubelet(slot, position, face2color, cube); } break; case UnityWebRequest.Result.InProgress: default: throw new ArgumentOutOfRangeException(); } } private void UpdateCube(string action = "") { StartCoroutine(_UpdateCube(action)); }
最後に,Cube を初期化して,キーボードのキーと action をくっつければ完成.
private void Awake() { // Handle input. InputActionMap cubeActionMap = new InputActionMap(); foreach (var face in faces) { string faceUpper = face.ToString(); string faceLower = faceUpper.ToLower(); // Upper case InputAction faceAction = cubeActionMap.AddAction(faceUpper, type: InputActionType.Button); faceAction.AddCompositeBinding("OneModifier") .With("Binding", $"<Keyboard>/{faceLower}") .With("Modifier", "<Keyboard>/shift"); faceAction.performed += context => { UpdateCube(faceUpper); }; // Lower case cubeActionMap.AddAction($"{faceUpper}'", binding: $"<Keyboard>/{faceLower}", type: InputActionType.Button).performed += context => { UpdateCube(faceLower); }; } cubeActionMap.AddAction("esc", binding: "<Keyboard>/escape", type: InputActionType.Button).performed += context => { ResetCube(); }; cubeActionMap.Enable(); } void Start() { // Initialize cube. cube = new GameObject("Cube"); ResetCube(); } private void ResetCube() { currentActions = ""; UpdateCube(); }
実際に試してみれば,各面が想定通りに動ける.
Script 全体:
using System; using System.Collections; using System.Collections.Generic; using System.Linq; using UnityEngine; using UnityEngine.Networking; using Newtonsoft.Json; using UnityEngine.InputSystem; public class TS1GM : MonoBehaviour { // Faces private readonly char[] faces = { 'U', 'D', 'F', 'B', 'L', 'R' }; // Colors private readonly Dictionary<char, Color> colorDict = new() { {'K', new Color(0f, 0f, 0f)}, {'B', new Color(0f, 70f / 255f, 173f / 255f)}, {'G', new Color(0f, 155f / 255f, 72f / 255f)}, {'O', new Color(255f / 255f, 88f / 255f, 0f)}, {'R', new Color(183f / 255f, 18f / 255f, 52f / 255f)}, {'W', new Color(1f, 1f, 1f)}, {'Y', new Color(255f / 255f, 213f / 255f, 0f)} }; // Position and Scale of stickers. private readonly Dictionary<char, Vector3> stickerPositionDict = new() { {'U', new Vector3(0f, 0.49f, 0f)}, {'D', new Vector3(0f, -0.49f, 0f)}, {'F', new Vector3( 0.49f, 0f, 0f)}, {'B', new Vector3(-0.49f, 0f, 0f)}, {'L', new Vector3(0f, 0f, -0.49f)}, {'R', new Vector3(0f, 0f, 0.49f)}, }; private readonly Dictionary<char, Vector3> stickerScaleDict = new() { {'U', new Vector3(0.85f, 0.04f, 0.85f)}, {'D', new Vector3(0.85f, 0.04f, 0.85f)}, {'F', new Vector3(0.04f, 0.85f, 0.85f)}, {'B', new Vector3(0.04f, 0.85f, 0.85f)}, {'L', new Vector3(0.85f, 0.85f, 0.04f)}, {'R', new Vector3(0.85f, 0.85f, 0.04f)}, }; // Position of cubelets. private readonly Dictionary<char, Vector3> cubeletPositionDict = new() { {'U', new Vector3(0f, 1f, 0f)}, {'D', new Vector3(0f, -1f, 0f)}, {'F', new Vector3( 1f, 0f, 0f)}, {'B', new Vector3(-1f, 0f, 0f)}, {'L', new Vector3(0f, 0f, -1f)}, {'R', new Vector3(0f, 0f, 1f)}, }; // URL private const string URL = "http://127.0.0.1:8000/tests/cube_class/"; // Cube Object private GameObject cube; private string currentActions = ""; private void Awake() { // Handle input. InputActionMap cubeActionMap = new InputActionMap(); foreach (var face in faces) { string faceUpper = face.ToString(); string faceLower = faceUpper.ToLower(); // Upper case InputAction faceAction = cubeActionMap.AddAction(faceUpper, type: InputActionType.Button); faceAction.AddCompositeBinding("OneModifier") .With("Binding", $"<Keyboard>/{faceLower}") .With("Modifier", "<Keyboard>/shift"); faceAction.performed += context => { UpdateCube(faceUpper); }; // Lower case cubeActionMap.AddAction($"{faceUpper}'", binding: $"<Keyboard>/{faceLower}", type: InputActionType.Button).performed += context => { UpdateCube(faceLower); }; } cubeActionMap.AddAction("esc", binding: "<Keyboard>/escape", type: InputActionType.Button).performed += context => { ResetCube(); }; cubeActionMap.Enable(); } void Start() { // Initialize cube. cube = new GameObject("Cube"); ResetCube(); } private void CreateCubelet(string cubeletName, Vector3 position, Dictionary<char, char> face2color, GameObject parentObj) { // Initialize. GameObject cubelet = GameObject.CreatePrimitive(PrimitiveType.Cube); cubelet.name = cubeletName; cubelet.GetComponent<MeshRenderer>().material.color = colorDict['K']; // Set up stickers. foreach (char face in faces) { GameObject sticker = GameObject.CreatePrimitive(PrimitiveType.Cube); sticker.name = face.ToString(); // Set position, scale. sticker.transform.position = stickerPositionDict[face]; sticker.transform.localScale = stickerScaleDict[face]; sticker.GetComponent<MeshRenderer>().material.color = face2color.TryGetValue(face, out char color) ? colorDict[color] : colorDict['K']; // Grouping. sticker.transform.SetParent(cubelet.transform); } cubelet.transform.position = position; cubelet.transform.SetParent(parentObj.transform); } private IEnumerator _UpdateCube(string actions) { currentActions += actions; // Request Json response. string uri = string.IsNullOrEmpty(currentActions) ? URL : $"{URL}?actions={currentActions}"; print(uri); using var webRequest = UnityWebRequest.Get(uri); yield return webRequest.SendWebRequest(); string[] pages = uri.Split('/'); int page = pages.Length - 1; switch (webRequest.result) { case UnityWebRequest.Result.ConnectionError: case UnityWebRequest.Result.DataProcessingError: case UnityWebRequest.Result.ProtocolError: Debug.LogError($"{pages[page]}: Error: {webRequest.error}"); break; case UnityWebRequest.Result.Success: // Convert Json to Dictionary. var jsonDict = JsonConvert.DeserializeObject<Dictionary<string, Dictionary<char, char>>>(webRequest.downloadHandler.text); // Reinitialize cube. Destroy(cube); cube = new GameObject("Cube"); // Set up cubelets. foreach (var (slot, face2color) in jsonDict) { Vector3 position = slot.Aggregate(Vector3.zero, (_position, face) => _position + cubeletPositionDict[face]); CreateCubelet(slot, position, face2color, cube); } break; case UnityWebRequest.Result.InProgress: default: throw new ArgumentOutOfRangeException(); } } private void UpdateCube(string action = "") { StartCoroutine(_UpdateCube(action)); } private void ResetCube() { currentActions = ""; UpdateCube(); } }