iTAC_Technical_Documents

アイタックソリューションズ株式会社

ブログ名

AI でルービックキューブを揃う(その2)Unity で目視ユニットテストを兼ねて可視化する

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 の形で渡し,_UpdateCubeC# の辞書にしてここに渡す.
  • 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();
    }
}