U3D之Editor擴展學習


 Unity3D提供了強大的編輯器擴展機制,在項目開發中,如果可以將一些繁瑣的工作放在編輯器擴展中進行,則會大大提高效率。本文對編輯器擴展進行了一些總結,希望對有興趣編寫編輯器擴展的開發人員有所幫助。當我們編寫一個編輯器擴展時,一般可以從以下四個類繼承:

1 . ScriptableObject  

最常見的小功能擴展,一般不用窗口的編輯擴展,可以從這個類中繼承,如以下代碼所示:

 

using UnityEngine;
using UnityEditor;
using System.Collections;

public class AddChild : ScriptableObject
{
    [MenuItem ("GameObject/Add Child ^n")]
    static void MenuAddChild()
    {
        Transform[] transforms = Selection.GetTransforms(SelectionMode.TopLevel | SelectionMode.OnlyUserModifiable);

        foreach(Transform transform in transforms)
        {
            GameObject newChild = new GameObject("_Child");
            newChild.transform.parent = transform;
        }
    }
}

 

這個擴展腳本從菜單的“GameObject->Add Child”啟動,功能是給Hierarchy窗口中選中的對GameObject添加一個名字為“_Child”的子GameObject,這樣可以免去從Hierarchy窗口的根節點拖拽新創建的GameObject到當前選中節點的麻煩,因為在Unity3D編輯器中,創建一個EmptyObject會在Hierarchy窗口的根節點出現,無論當前選中的節點對象是哪個。

 

2 .ScriptableWizard      

需要對擴展的參數進行設置,然后再進行功能觸發的,可以從這個類進行派生。它已經定制好了四個消息響應函數,開發者對其進行填充即可。

(1) OnWizardUpdate  

當擴展窗口打開時或用戶對窗口的內容進行改動時,會調用此函數。一般會在這里面顯示幫助文字和進行內容有效性驗證;

(2)OnWizardCreate  

這是用戶點擊窗口的Create按鈕時進行的操作,從ScriptableWizard的名字可以看出,這是一種類似向導的窗口 ,而這種窗口我們在Visual Studio中經常會使用到,如下圖:

 

只不過Unity3D中的ScriptableWizard窗口只能進行小於或等於兩個按鈕的定制,一個就是所謂的Create按鈕,另外一個則籠統稱之為Other按鈕。ScriptableWizard.DisplayWizard這個靜態函數用於對ScriptableWizard窗口標題和按鈕名字的定制。

(3) OnDrawGizmos

在窗口可見時,每一幀都會調用這個函數。在其中進行Gizmos的繪制,也就是輔助編輯的線框體。Unity的Gizmos類提供了DrawRayDrawLine ,DrawWireSphere ,DrawSphere ,DrawWireCube ,DrawCubeDrawIcon ,DrawGUITexture 功能。這個功能在Unity3D 的3.4版本中測試了一下,發現沒有任何Gizmos繪制出來難過

(4) OnWizardOtherButton

本文在(2) 中已經提及ScriptableWizard窗口最多可以定制兩個按鈕,一個是Create,另外一個稱之為Other,這個函數會在other按鈕被點擊時調用。下面是一個使用ScriptableWizard進行編輯擴展的例子:

 

<span style="font-size: 18px;">using UnityEditor;
using UnityEngine;
using System.Collections;

/// <summary>
/// 對於選定GameObject,進行指定component的批量添加
/// </summary>
public class AddRemoveComponentsRecursively : ScriptableWizard
{
    public string componentType = null;

    /// <summary>
    /// 當沒有任何GameObject被選中的時候,將菜單disable(注意,這個函數名可以隨意取)
    /// </summary>
    /// <returns></returns>
    [MenuItem("GameObject/Add or remove components recursively...", true)]
    static bool CreateWindowDisabled()
    {
        return Selection.activeTransform;
    }

    /// <summary>
    /// 創建編輯窗口(注意,這個函數名可以隨意取)
    /// </summary>
    [MenuItem("GameObject/Add or remove components recursively...")]
    static void CreateWindow()
    {
        // 定制窗口標題和按鈕,其中第二個參數是Create按鈕,第三個則屬於other按鈕
        // 如果不想使用other按鈕,則可調用DisplayWizard的兩參數版本
        ScriptableWizard.DisplayWizard<AddRemoveComponentsRecursively>(
            "Add or remove components recursivly",
            "Add", "Remove");
    }

    /// <summary>
    /// 窗口創建或窗口內容更改時調用
    /// </summary>
    void OnWizardUpdate()
    {
        helpString = "Note: Duplicates are not created";

        if (string.IsNullOrEmpty(componentType))
        {
            errorString = "Please enter component class name";
            isValid = false;
        }
        else
        {
            errorString = "";
            isValid = true;
        }
    }

    /// <summary>
    /// 點擊Add按鈕(即Create按鈕)調用
    /// </summary>
    void OnWizardCreate()
    {
        int c = 0;
        Transform[] ts = Selection.GetTransforms(SelectionMode.Deep);
        foreach (Transform t in ts)
        {
            if (t.gameObject.GetComponent(componentType) == null)
            {
                if (t.gameObject.AddComponent(componentType) == null)
                {
                    Debug.LogWarning("Component of type " + componentType + " does not exist");
                    return;
                }
                c++;
            }
        }
        Debug.Log("Added " + c + " components of type " + componentType);
    }

    /// <summary>
    /// 點擊Remove(即other按鈕)調用
    /// </summary>
    void OnWizardOtherButton()
    {
        int c = 0;
        Transform[] ts = Selection.GetTransforms(SelectionMode.Deep);
        foreach (Transform t in ts)
        {
            if (t.GetComponent(componentType) != null)
            {
                DestroyImmediate(t.GetComponent(componentType));
                c++;
            }
        }
        Debug.Log("Removed " + c + " components of type " + componentType);
        Close();
    }
}</span>

其運行窗口如下所示:

 

3 . EditorWindow

 

較復雜的功能,需要多個靈活的控件,實現自由浮動和加入其他窗口的tab,可以從這個類派生,這種窗口的窗體功能和Scene,Hierarchy等窗口完全一致。下面這個例子實現了GameObject的空間對齊和拷貝(也就是將GameObject A作為基准,選中其他的GameObject進行對准或空間位置拷貝),對齊和拷貝提高了了開發者擺放物件的效率;另外還有隨機和噪聲,后兩者用於擺放大量同類物件的時候可以使用,比如一大堆散落的瓶子。

 

 

<span style="font-size: 18px;">// /////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Transform Utilities.
//
// This window contains four useful tools for asset placing and manipulation: Align, Copy, Randomize and Add noise.
//
// Put this into Assets/Editor and once compiled by Unity you find
// the new functionality in Window -> TransformUtilities, or simply press Ctrl+t (Cmd+t for Mac users)
// 
// Developed by Daniel 
// http://www.silentkraken.com
// e-mail: seth@silentkraken.com
//
// /////////////////////////////////////////////////////////////////////////////////////////////////////////

using UnityEngine;
using UnityEditor;

public class TransformUtilitiesWindow : EditorWindow 
{
    //Window control values
    public int toolbarOption = 0;
    public string[] toolbarTexts = {"Align", "Copy", "Randomize", "Add noise"};

    private bool xCheckbox = true;
    private bool yCheckbox = true;
    private bool zCheckbox = true;

    private Transform source;
    private float randomRangeMin = 0f;
    private float randomRangeMax = 1f;
    private int alignSelectionOption = 0;
    private int alignSourceOption = 0;

    /// <summary>
    /// Retrives the TransformUtilities window or creates a new one
    /// </summary>
    [MenuItem("Window/TransformUtilities %t")]
    static void Init()
    {
        TransformUtilitiesWindow window = (TransformUtilitiesWindow)EditorWindow.GetWindow(typeof(TransformUtilitiesWindow));
        window.Show();
    }
    
    /// <summary>
    /// Window drawing operations
    /// </summary>
    void OnGUI () 
    {
        toolbarOption = GUILayout.Toolbar(toolbarOption, toolbarTexts);
        switch (toolbarOption)
        {
            case 0:
                CreateAxisCheckboxes("Align");
                CreateAlignTransformWindow();
                break;
            case 1:
                CreateAxisCheckboxes("Copy");
                CreateCopyTransformWindow();
                break;
            case 2:
                CreateAxisCheckboxes("Randomize");
                CreateRandomizeTransformWindow();
                break;
            case 3:
                CreateAxisCheckboxes("Add noise");
                CreateAddNoiseToTransformWindow();
                break;
        }
    }

    /// <summary>
    /// Draws the 3 axis checkboxes (x y z)
    /// </summary>
    /// <param name="operationName"></param>
    private void CreateAxisCheckboxes(string operationName)
    {
        GUILayout.Label(operationName + " on axis", EditorStyles.boldLabel);

        GUILayout.BeginHorizontal();
            xCheckbox = GUILayout.Toggle(xCheckbox, "X");
            yCheckbox = GUILayout.Toggle(yCheckbox, "Y");
            zCheckbox = GUILayout.Toggle(zCheckbox, "Z");
        GUILayout.EndHorizontal();

        EditorGUILayout.Space();
    }

    /// <summary>
    /// Draws the range min and max fields
    /// </summary>
    private void CreateRangeFields()
    {
        GUILayout.Label("Range", EditorStyles.boldLabel);
        GUILayout.BeginHorizontal();
        randomRangeMin = EditorGUILayout.FloatField("Min:", randomRangeMin);
        randomRangeMax = EditorGUILayout.FloatField("Max:", randomRangeMax);
        GUILayout.EndHorizontal();
        EditorGUILayout.Space();
    }

    /// <summary>
    /// Creates the Align transform window
    /// </summary>
    private void CreateAlignTransformWindow()
    {
        //Source transform
        GUILayout.BeginHorizontal();
        GUILayout.Label("Align to: \t");
        source = EditorGUILayout.ObjectField(source, typeof(Transform)) as Transform;
        GUILayout.EndHorizontal();

        string[] texts = new string[4] { "Min", "Max", "Center", "Pivot" };

        //Display align options
        EditorGUILayout.BeginHorizontal();
        EditorGUILayout.BeginVertical();
        GUILayout.Label("Selection:", EditorStyles.boldLabel);
        alignSelectionOption = GUILayout.SelectionGrid(alignSelectionOption, texts, 1);
        EditorGUILayout.EndVertical();
        EditorGUILayout.BeginVertical();
        GUILayout.Label("Source:", EditorStyles.boldLabel);
        alignSourceOption = GUILayout.SelectionGrid(alignSourceOption, texts, 1);
        EditorGUILayout.EndVertical();
        EditorGUILayout.EndHorizontal();

        EditorGUILayout.Space();

        //Position
        if (GUILayout.Button("Align"))
        {
            if (source != null)
            {
                //Add a temporary box collider to the source if it doesn't have one
                Collider sourceCollider = source.collider;
                bool destroySourceCollider = false;
                if (sourceCollider == null)
                {
                    sourceCollider = source.gameObject.AddComponent<BoxCollider>();
                    destroySourceCollider = true;
                }

                foreach (Transform t in Selection.transforms)
                {
                    //Add a temporary box collider to the transform if it doesn't have one
                    Collider transformCollider = t.collider;
                    bool destroyTransformCollider = false;
                    if (transformCollider == null)
                    {
                        transformCollider = t.gameObject.AddComponent<BoxCollider>();
                        destroyTransformCollider = true;
                    }

                    Vector3 sourceAlignData = new Vector3();
                    Vector3 transformAlignData = new Vector3();

                    //Transform
                    switch (alignSelectionOption)
                    {
                        case 0: //Min
                            transformAlignData = transformCollider.bounds.min;
                            break;
                        case 1: //Max
                            transformAlignData = transformCollider.bounds.max;
                            break;
                        case 2: //Center
                            transformAlignData = transformCollider.bounds.center;
                            break;
                        case 3: //Pivot
                            transformAlignData = transformCollider.transform.position;
                            break;
                    }

                    //Source
                    switch (alignSourceOption)
                    {
                        case 0: //Min
                            sourceAlignData = sourceCollider.bounds.min;
                            break;
                        case 1: //Max
                            sourceAlignData = sourceCollider.bounds.max;
                            break;
                        case 2: //Center
                            sourceAlignData = sourceCollider.bounds.center;
                            break;
                        case 3: //Pivot
                            sourceAlignData = sourceCollider.transform.position;
                            break;
                    }

                    Vector3 tmp = new Vector3();
                    tmp.x = xCheckbox ? sourceAlignData.x - (transformAlignData.x - t.position.x) : t.position.x;
                    tmp.y = yCheckbox ? sourceAlignData.y - (transformAlignData.y - t.position.y) : t.position.y;
                    tmp.z = zCheckbox ? sourceAlignData.z - (transformAlignData.z - t.position.z) : t.position.z;

                    //Register the Undo
                    Undo.RegisterUndo(t, "Align " + t.gameObject.name + " to " + source.gameObject.name);
                    t.position = tmp;
                    
                    //Ugly hack!
                    //Unity needs to update the collider of the selection to it's new position
                    //(it stores in cache the collider data)
                    //We can force the update by a change in a public variable (shown in the inspector), 
                    //then a call SetDirty to update the collider (it won't work if all inspector variables are the same).
                    //But we want to restore the changed property to what it was so we do it twice.
                    transformCollider.isTrigger = !transformCollider.isTrigger;
                    EditorUtility.SetDirty(transformCollider);
                    transformCollider.isTrigger = !transformCollider.isTrigger;
                    EditorUtility.SetDirty(transformCollider);

                    //Destroy the collider we added
                    if (destroyTransformCollider)
                    {
                        DestroyImmediate(transformCollider);
                    }
                }

                //Destroy the collider we added
                if (destroySourceCollider)
                {
                    DestroyImmediate(sourceCollider);
                }
            }
            else
            {
                EditorUtility.DisplayDialog("Error", "There is no source transform", "Ok");
                EditorApplication.Beep();
            }
        }
    }

    /// <summary>
    /// Creates the copy transform window
    /// </summary>
    private void CreateCopyTransformWindow()
    {
        //Source transform
        GUILayout.BeginHorizontal();
            GUILayout.Label("Copy from: \t");
            source = EditorGUILayout.ObjectField(source, typeof(Transform)) as Transform;
        GUILayout.EndHorizontal();

        EditorGUILayout.Space();

        //Position
        if (GUILayout.Button("Copy Position"))
        {
            if (source != null)
            {
                foreach (Transform t in Selection.transforms)
                {
                    Vector3 tmp = new Vector3();
                    tmp.x = xCheckbox ? source.position.x : t.position.x;
                    tmp.y = yCheckbox ? source.position.y : t.position.y;
                    tmp.z = zCheckbox ? source.position.z : t.position.z;

                    Undo.RegisterUndo(t, "Copy position");
                    t.position = tmp;
                }
            }
            else
            {
                EditorUtility.DisplayDialog("Error", "There is no source transform", "Ok");
                EditorApplication.Beep();
            }
        }

        //Rotation
        if (GUILayout.Button("Copy Rotation"))
        {
            if (source != null)
            {
                foreach (Transform t in Selection.transforms)
                {
                    Vector3 tmp = new Vector3();
                    tmp.x = xCheckbox ? source.rotation.eulerAngles.x : t.rotation.eulerAngles.x;
                    tmp.y = yCheckbox ? source.rotation.eulerAngles.y : t.rotation.eulerAngles.y;
                    tmp.z = zCheckbox ? source.rotation.eulerAngles.z : t.rotation.eulerAngles.z;
                    Quaternion tmp2 = t.rotation;
                    tmp2.eulerAngles = tmp;

                    Undo.RegisterUndo(t, "Copy rotation");
                    t.rotation = tmp2;
                }
            }
            else
            {
                EditorUtility.DisplayDialog("Error", "There is no source transform", "Ok");
                EditorApplication.Beep();
            }
        }

        //Local Scale
        if (GUILayout.Button("Copy Local Scale"))
        {
            if (source != null)
            {
                foreach (Transform t in Selection.transforms)
                {
                    Vector3 tmp = new Vector3();
                    tmp.x = xCheckbox ? source.localScale.x : t.localScale.x;
                    tmp.y = yCheckbox ? source.localScale.y : t.localScale.y;
                    tmp.z = zCheckbox ? source.localScale.z : t.localScale.z;

                    Undo.RegisterUndo(t, "Copy local scale");
                    t.localScale = tmp;
                }
            }
            else
            {
                EditorUtility.DisplayDialog("Error", "There is no source transform", "Ok");
                EditorApplication.Beep();
            }
        }
    }

    /// <summary>
    /// Creates the Randomize transform window
    /// </summary>
    private void CreateRandomizeTransformWindow()
    {
        CreateRangeFields();

        //Position
        if (GUILayout.Button("Randomize Position"))
        {
            foreach (Transform t in Selection.transforms)
            {
                Vector3 tmp = new Vector3();
                tmp.x = xCheckbox ? Random.Range(randomRangeMin, randomRangeMax) : t.position.x;
                tmp.y = yCheckbox ? Random.Range(randomRangeMin, randomRangeMax) : t.position.y;
                tmp.z = zCheckbox ? Random.Range(randomRangeMin, randomRangeMax) : t.position.z;

                Undo.RegisterUndo(t, "Randomize position");
                t.position = tmp;
            }
        }

        //Rotation
        if (GUILayout.Button("Randomize Rotation"))
        {
            foreach (Transform t in Selection.transforms)
            {
                Vector3 tmp = new Vector3();
                tmp.x = xCheckbox ? Random.Range(randomRangeMin, randomRangeMax) : t.rotation.eulerAngles.x;
                tmp.y = yCheckbox ? Random.Range(randomRangeMin, randomRangeMax) : t.rotation.eulerAngles.y;
                tmp.z = zCheckbox ? Random.Range(randomRangeMin, randomRangeMax) : t.rotation.eulerAngles.z;
                Quaternion tmp2 = t.rotation;
                tmp2.eulerAngles = tmp;

                Undo.RegisterUndo(t, "Randomize rotation");
                t.rotation = tmp2;
            }
        }

        //Local Scale
        if (GUILayout.Button("Randomize Local Scale"))
        {
            foreach (Transform t in Selection.transforms)
            {
                Vector3 tmp = new Vector3();
                tmp.x = xCheckbox ? Random.Range(randomRangeMin, randomRangeMax) : t.localScale.x;
                tmp.y = yCheckbox ? Random.Range(randomRangeMin, randomRangeMax) : t.localScale.y;
                tmp.z = zCheckbox ? Random.Range(randomRangeMin, randomRangeMax) : t.localScale.z;

                Undo.RegisterUndo(t, "Randomize local scale");
                t.localScale = tmp;
            }
        }
    }

    /// <summary>
    /// Creates the Add Noise To Transform window
    /// </summary>
    private void CreateAddNoiseToTransformWindow()
    {
        CreateRangeFields();

        //Position
        if (GUILayout.Button("Add noise to Position"))
        {
            foreach (Transform t in Selection.transforms)
            {
                Vector3 tmp = new Vector3();
                tmp.x = xCheckbox ? Random.Range(randomRangeMin, randomRangeMax) : 0;
                tmp.y = yCheckbox ? Random.Range(randomRangeMin, randomRangeMax) : 0;
                tmp.z = zCheckbox ? Random.Range(randomRangeMin, randomRangeMax) : 0;

                Undo.RegisterUndo(t, "Add noise to position");
                t.position += tmp;
            }
        }

        //Rotation
        if (GUILayout.Button("Add noise to Rotation"))
        {
            foreach (Transform t in Selection.transforms)
            {
                Vector3 tmp = new Vector3();
                tmp.x = xCheckbox ?  t.rotation.eulerAngles.x + Random.Range(randomRangeMin, randomRangeMax) : 0;
                tmp.y = yCheckbox ?  t.rotation.eulerAngles.y + Random.Range(randomRangeMin, randomRangeMax) : 0;
                tmp.z = zCheckbox ?  t.rotation.eulerAngles.z + Random.Range(randomRangeMin, randomRangeMax) : 0;

                Undo.RegisterUndo(t, "Add noise to rotation");
                t.rotation = Quaternion.Euler(tmp);
            }
        }

        //Local Scale
        if (GUILayout.Button("Add noise to Local Scale"))
        {
            foreach (Transform t in Selection.transforms)
            {
                Vector3 tmp = new Vector3();
                tmp.x = xCheckbox ? Random.Range(randomRangeMin, randomRangeMax) : 0;
                tmp.y = yCheckbox ? Random.Range(randomRangeMin, randomRangeMax) : 0;
                tmp.z = zCheckbox ? Random.Range(randomRangeMin, randomRangeMax) : 0;

                Undo.RegisterUndo(t, "Add noise to local scale");
                t.localScale += tmp;
            }
        }
    }
}</span>

其窗口如下圖所示:

 

4. Editor

對某自定義組件進行觀察的Inspector窗口,可以從它派生。如下代碼所示:

代碼片段1定義了一個名為Star的組件:

 

<span style="font-size: 18px;">using System;
using UnityEngine;

[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class Star : MonoBehaviour {

    [Serializable]
    public class Point {
        public Color color;
        public Vector3 offset;
    }

    public Point[] points;
    public int frequency = 1;
    public Color centerColor;

    private Mesh mesh;
    private Vector3[] vertices;
    private Color[] colors;
    private int[] triangles;

    void Start () {
        GetComponent<MeshFilter>().mesh = mesh = new Mesh();
        mesh.name = "Star Mesh";

        if(frequency < 1){
            frequency = 1;
        }
        if(points == null || points.Length == 0){
            points = new Point[]{ new Point()};
        }

        int numberOfPoints = frequency * points.Length;
        vertices = new Vector3[numberOfPoints + 1];
        colors = new Color[numberOfPoints + 1];
        triangles = new int[numberOfPoints * 3];
        float angle = -360f / numberOfPoints;
        colors[0] = centerColor;
        for(int iF = 0, v = 1, t = 1; iF < frequency; iF++){
            for(int iP = 0; iP < points.Length; iP += 1, v += 1, t += 3){
                vertices[v] = Quaternion.Euler(0f, 0f, angle * (v - 1)) * points[iP].offset;
                colors[v] = points[iP].color;
                triangles[t] = v;
                triangles[t + 1] = v + 1;
            }
        }
        triangles[triangles.Length - 1] = 1;

        mesh.vertices = vertices;
        mesh.colors = colors;
        mesh.triangles = triangles;
    }
}</span>

代碼片段2定義了對Star組件進行觀測的Inspector窗口:

<span style="font-size: 18px;">using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(Star))]
public class StarInspector : Editor {

    private static GUIContent
        insertContent = new GUIContent("+", "duplicate this point"),
        deleteContent = new GUIContent("-", "delete this point"),
        pointContent = GUIContent.none;

    private static GUILayoutOption
        buttonWidth = GUILayout.MaxWidth(20f),
        colorWidth = GUILayout.MaxWidth(50f);

    private SerializedObject star;
    private SerializedProperty
        points,
        frequency,
        centerColor;

    void OnEnable () { … }

    public override void OnInspectorGUI () {
        star.Update();

        GUILayout.Label("Points");
        for(int i = 0; i < points.arraySize; i++){
            EditorGUILayout.BeginHorizontal();
            SerializedProperty point = points.GetArrayElementAtIndex(i);
            EditorGUILayout.PropertyField(point.FindPropertyRelative("offset"), pointContent);
            EditorGUILayout.PropertyField(point.FindPropertyRelative("color"), pointContent, colorWidth);

            if(GUILayout.Button(insertContent, EditorStyles.miniButtonLeft, buttonWidth)){
                points.InsertArrayElementAtIndex(i);
            }
            if(GUILayout.Button(deleteContent, EditorStyles.miniButtonRight, buttonWidth)){
                points.DeleteArrayElementAtIndex(i);
            }

            EditorGUILayout.EndHorizontal();
        }

        EditorGUILayout.PropertyField(frequency);
        EditorGUILayout.PropertyField(centerColor);

        star.ApplyModifiedProperties();
    }
}</span>

其Inspector窗口如下圖所示:

說到這里,大家對ScriptableObject, ScriptableWizard, EditorWindow和Editor應該都有應有了一定了解。其中EditorWindow和Editor都繼承了ScriptableObject,而ScritableWizard則繼承了EditorWindow派。在實際開發應用中,應該根據需求的特點,靈活使用這四個類進行編輯器擴展。

參考資料:

1. http://catlikecoding.com/unity/tutorials/star/

2. http://www.unifycommunity.com/wiki

3. http://www.blog.silentkraken.com/2010/02/06/transformutilities/

4.http://unity3d.com/support/documentation/ScriptReference

 

轉:http://blog.csdn.net/jjiss318/article/details/7435708

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM