5016 lines
198 KiB
C#
5016 lines
198 KiB
C#
// Copyright (c) 2024 Synty Studios Limited. All rights reserved.
|
|
//
|
|
// Use of this software is subject to the terms and conditions of the Synty Studios End User Licence Agreement (EULA)
|
|
// available at: https://syntystore.com/pages/end-user-licence-agreement
|
|
//
|
|
// For additional details, see the LICENSE.MD file bundled with this software.
|
|
|
|
#if UNITY_EDITOR
|
|
|
|
using Synty.SidekickCharacters.API;
|
|
using Synty.SidekickCharacters.Database;
|
|
using Synty.SidekickCharacters.Database.DTO;
|
|
using Synty.SidekickCharacters.Enums;
|
|
using Synty.SidekickCharacters.Filters;
|
|
using Synty.SidekickCharacters.Serialization;
|
|
using Synty.SidekickCharacters.SkinnedMesh;
|
|
using Synty.SidekickCharacters.Synty.SidekickCharacters.Scripts.Editor.UI;
|
|
using Synty.SidekickCharacters.Utils;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
using Unity.VisualScripting.YamlDotNet.Serialization;
|
|
using UnityEditor;
|
|
using UnityEditor.Animations;
|
|
using UnityEditor.UIElements;
|
|
using UnityEngine;
|
|
using UnityEngine.UIElements;
|
|
using Debug = UnityEngine.Debug;
|
|
using Object = UnityEngine.Object;
|
|
using Random = UnityEngine.Random;
|
|
|
|
namespace Synty.SidekickCharacters
|
|
{
|
|
public class ModularCharacterWindow : EditorWindow
|
|
{
|
|
private const string _AUTOSAVE_KEY = "SK_Autosave_character";
|
|
private const string _AUTOSAVE_MISSING_PARTS = "SK_Autosave_missing_parts";
|
|
private const string _AUTOSAVE_STATE = "SK_Autosave_state";
|
|
private const string _BASE_COLOR_SET_NAME = "Species";
|
|
private const string _BASE_COLOR_SET_PATH = "Assets/External/Models/SidekickCharacters/Resources/Species";
|
|
private const string _BASE_MESH_NAME = "Meshes/SK_BaseModel";
|
|
private const string _BASE_MATERIAL_NAME = "Materials/M_BaseMaterial";
|
|
private const string _BLEND_GENDER_NAME = "masculineFeminine";
|
|
private const string _BLEND_MUSCLE_NAME = "defaultBuff";
|
|
private const string _BLEND_SHAPE_HEAVY_NAME = "defaultHeavy";
|
|
private const string _BLEND_SHAPE_SKINNY_NAME = "defaultSkinny";
|
|
private const string _AUTO_OPEN_STATE = "syntySkAutoOpenState";
|
|
private const string _OUTPUT_MODEL_NAME = "Combined Character";
|
|
private const string _PART_COUNT_BODY = " parts in library";
|
|
private const string _TEXTURE_COLOR_NAME = "ColorMap.png";
|
|
private const string _TEXTURE_METALLIC_NAME = "MetallicMap.png";
|
|
private const string _TEXTURE_SMOOTHNESS_NAME = "SmoothnessMap.png";
|
|
private const string _TEXTURE_REFLECTION_NAME = "ReflectionMap.png";
|
|
private const string _TEXTURE_EMISSION_NAME = "EmissionMap.png";
|
|
private const string _TEXTURE_OPACITY_NAME = "OpacityMap.png";
|
|
private const string _TEXTURE_PREFIX = "T_";
|
|
|
|
private static readonly int _COLOR_MAP = Shader.PropertyToID("_ColorMap");
|
|
private static readonly int _METALLIC_MAP = Shader.PropertyToID("_MetallicMap");
|
|
private static readonly int _SMOOTHNESS_MAP = Shader.PropertyToID("_SmoothnessMap");
|
|
private static readonly int _REFLECTION_MAP = Shader.PropertyToID("_ReflectionMap");
|
|
private static readonly int _EMISSION_MAP = Shader.PropertyToID("_EmissionMap");
|
|
private static readonly int _OPACITY_MAP = Shader.PropertyToID("_OpacityMap");
|
|
private static Queue<Action> _callbackQueue = new Queue<Action>();
|
|
private static bool _openWindowOnStart = true;
|
|
private readonly List<SidekickColorRow> _visibleColorRows = new List<SidekickColorRow>();
|
|
|
|
private List<SidekickColorRow> _allColorRows = new List<SidekickColorRow>();
|
|
private List<SidekickPart> _allParts;
|
|
private List<SidekickSpecies> _allSpecies;
|
|
private ObjectField _animationField;
|
|
private FilterGroup _appliedPartFilters;
|
|
private bool _applyingPreset = false;
|
|
private List<SidekickPart> _availablePartList;
|
|
List<SidekickPartPreset> _availablePresets;
|
|
private bool _bakeBlends = true;
|
|
private ObjectField _baseModelField;
|
|
private Dictionary<string, Vector3> _blendShapeRigMovement = new Dictionary<string, Vector3>();
|
|
private Dictionary<string, Quaternion> _blendShapeRigRotation = new Dictionary<string, Quaternion>();
|
|
private ToolbarToggle _bodyPartsTab;
|
|
private ToolbarToggle _bodyPresetTab;
|
|
private ToolbarToggle _bodyShapeTab;
|
|
private ScrollView _bodyShapeView;
|
|
private float _bodySizeHeavyBlendValue;
|
|
private float _bodySizeSkinnyBlendValue;
|
|
private Slider _bodySizeSlider;
|
|
private Slider _bodyTypeSlider;
|
|
private VisualElement _colorSelectionRowView;
|
|
private ToolbarToggle _colorSelectionTab;
|
|
private ScrollView _colorSelectionView;
|
|
private DropdownField _colorSetsDropdown;
|
|
private bool _combineMeshes = true;
|
|
private AnimatorController _currentAnimationController;
|
|
private Dictionary<string, SidekickBodyShapePreset> _currentBodyPresetDictionary = new Dictionary<string, SidekickBodyShapePreset>();
|
|
private Dictionary<CharacterPartType, SidekickPart> _currentCharacter = new Dictionary<CharacterPartType, SidekickPart>();
|
|
private Dictionary<string, SidekickColorPreset> _currentColorSpeciesPresetDictionary = new Dictionary<string, SidekickColorPreset>();
|
|
private Dictionary<string, SidekickColorPreset> _currentColorOutfitsPresetDictionary = new Dictionary<string, SidekickColorPreset>();
|
|
private Dictionary<string, SidekickColorPreset> _currentColorAttachmentsPresetDictionary = new Dictionary<string, SidekickColorPreset>();
|
|
private Dictionary<string, SidekickColorPreset> _currentColorMaterialsPresetDictionary = new Dictionary<string, SidekickColorPreset>();
|
|
private Dictionary<string, SidekickColorPreset> _currentColorElementsPresetDictionary = new Dictionary<string, SidekickColorPreset>();
|
|
private SidekickColorSet _currentColorSet;
|
|
private bool _currentGlobalLockStatus;
|
|
private Material _currentMaterial;
|
|
private Dictionary<CharacterPartType, string> _previousPartSelections;
|
|
private ColorPartType _currentPartType;
|
|
private Dictionary<string, SidekickPartPreset> _currentHeadPresetDictionary = new Dictionary<string, SidekickPartPreset>();
|
|
private Dictionary<string, SidekickPartPreset> _currentUpperBodyPresetDictionary = new Dictionary<string, SidekickPartPreset>();
|
|
private Dictionary<string, SidekickPartPreset> _currentLowerBodyPresetDictionary = new Dictionary<string, SidekickPartPreset>();
|
|
private SidekickSpecies _currentSpecies;
|
|
private TabView _currentTab;
|
|
private Dictionary<ColorPartType, List<Vector2>> _currentUVDictionary = new Dictionary<ColorPartType, List<Vector2>>();
|
|
private List<Vector2> _currentUVList = new List<Vector2>();
|
|
private DatabaseManager _dbManager;
|
|
private string _dbPath;
|
|
private ToolbarToggle _decalSelectionTab;
|
|
private ScrollView _decalSelectionView;
|
|
private StyleSheet _editorStyle;
|
|
private bool _showAllColourProperties = false;
|
|
private float _bodyTypeBlendValue;
|
|
private bool _loadingCharacter = false;
|
|
private bool _loadingContent = false;
|
|
private Image _loadingImage;
|
|
private ObjectField _materialField;
|
|
private float _musclesBlendValue;
|
|
private Slider _musclesSlider;
|
|
private GameObject _newModel;
|
|
private VisualElement _newSetNameContainer;
|
|
private ScrollView _optionSelectionView;
|
|
private ToolbarToggle _optionTab;
|
|
private Label _partCountLabel;
|
|
private Dictionary<CharacterPartType, SkinnedMeshRenderer> _partDictionary;
|
|
private Dictionary<CharacterPartType, Dictionary<string, string>> _partLibrary;
|
|
private Dictionary<CharacterPartType, List<SidekickPart>> _allPartsLibrary;
|
|
private Dictionary<string, List<string>> _partOutfitMap;
|
|
private Dictionary<PopupField<string>, bool> _partLockMap;
|
|
private Dictionary<CharacterPartType, PartTypeControls> _partSelectionDictionary;
|
|
private Foldout _partsFoldout;
|
|
private Dictionary<SidekickSpecies, List<string>> _partSpeciesMap;
|
|
private Dictionary<SidekickPresetFilter, bool> _partPresetFilterToggleMap = new Dictionary<SidekickPresetFilter, bool>();
|
|
private ScrollView _partView;
|
|
private Dictionary<string, string> _presetDefaultValues = new Dictionary<string, string>();
|
|
private VisualElement _presetPartContainer;
|
|
private ScrollView _presetView;
|
|
private Toggle _previewToggle;
|
|
private bool _processingSpeciesChange = false;
|
|
private bool _requiresWrap = false;
|
|
private VisualElement _root;
|
|
private bool _showMissingPartsPopup = false;
|
|
private SidekickRuntime _sidekickRuntime;
|
|
private DropdownField _speciesField;
|
|
private DropdownField _speciesPresetField;
|
|
private PlayModeStateChange _stateChange;
|
|
private bool _useAutoSaveAndLoad = false;
|
|
private List<SidekickColorSet> _visibleColorSets = new List<SidekickColorSet>();
|
|
|
|
// Animation variables
|
|
private Animator _currentAnimator;
|
|
private string _defaultStateName = "Idle";
|
|
private float _defaultClipDurationFallback = 2f;
|
|
private float _blendDuration = 0.5f; // seconds
|
|
private List<string> _otherStates = new List<string>();
|
|
private string _currentState;
|
|
private float _playbackTime;
|
|
private float _stateDuration = 1f;
|
|
private int _loopsRemaining = 0;
|
|
private bool _playingDefault = true;
|
|
private double _lastEditorTime;
|
|
private Transform[] _animatorBones;
|
|
private Dictionary<Transform, Vector3> _previousPosePos = new Dictionary<Transform, Vector3>();
|
|
private Dictionary<Transform, Quaternion> _previousPoseRot = new Dictionary<Transform, Quaternion>();
|
|
private float _blendElapsedTime = 0f;
|
|
private bool _blending = false;
|
|
|
|
/// <inheritdoc cref="Awake" />
|
|
private void Awake()
|
|
{
|
|
InitializeEditorWindow();
|
|
}
|
|
|
|
/// <inheritdoc cref="OnDestroy" />
|
|
private void OnDestroy()
|
|
{
|
|
if (_useAutoSaveAndLoad)
|
|
{
|
|
SerializedCharacter savedCharacter = CreateSerializedCharacter(_OUTPUT_MODEL_NAME);
|
|
Serializer serializer = new Serializer();
|
|
string serializedCharacter = serializer.Serialize(savedCharacter);
|
|
EditorPrefs.SetString(_AUTOSAVE_KEY, serializedCharacter);
|
|
}
|
|
|
|
// ensures we release the file lock on the database
|
|
_dbManager?.CloseConnection();
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
/// <inheritdoc cref="OnEnable" />
|
|
private void OnEnable()
|
|
{
|
|
EditorApplication.playModeStateChanged += StateChange;
|
|
EditorApplication.update += AnimationUpdate;
|
|
}
|
|
|
|
private void OnDisable()
|
|
{
|
|
EditorApplication.update -= AnimationUpdate;
|
|
}
|
|
|
|
/// <inheritdoc cref="Update" />
|
|
private void Update()
|
|
{
|
|
if (_loadingContent && _loadingImage != null)
|
|
{
|
|
Vector3 rotation = _loadingImage.transform.rotation.eulerAngles;
|
|
rotation += new Vector3(0, 0, 0.5f * Time.deltaTime);
|
|
_loadingImage.transform.rotation = Quaternion.Euler(rotation);
|
|
}
|
|
|
|
while (_callbackQueue.Count > 0)
|
|
{
|
|
Action action = null;
|
|
lock (_callbackQueue)
|
|
{
|
|
if (_callbackQueue.Count > 0)
|
|
{
|
|
action = _callbackQueue.Dequeue();
|
|
}
|
|
}
|
|
action?.Invoke();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets up the animation controllers for playing during runtime.
|
|
/// </summary>
|
|
/// <returns>True if it is set up; otherwise false.</returns>
|
|
private bool SetupAnimationControllers()
|
|
{
|
|
_loopsRemaining = 0;
|
|
|
|
_currentAnimator = _newModel.GetComponent<Animator>();
|
|
if (_currentAnimator != null)
|
|
{
|
|
_currentAnimationController = _currentAnimator.runtimeAnimatorController as AnimatorController;
|
|
_sidekickRuntime.CurrentAnimationController = _currentAnimationController;
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
|
|
_animatorBones = _currentAnimator.GetComponentsInChildren<Transform>();
|
|
CollectOtherStates();
|
|
|
|
if (string.IsNullOrEmpty(_currentState))
|
|
{
|
|
StartDefaultState();
|
|
}
|
|
else
|
|
{
|
|
SetState(_currentState);
|
|
}
|
|
|
|
_lastEditorTime = EditorApplication.timeSinceStartup;
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Plays the animation during edit time.
|
|
/// </summary>
|
|
private void AnimationUpdate()
|
|
{
|
|
if (Application.isPlaying)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (_animationField == null || _animationField.value == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (_currentAnimator == null || _currentAnimationController == null)
|
|
{
|
|
if (!SetupAnimationControllers())
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
double currentTime = EditorApplication.timeSinceStartup;
|
|
float deltaTime = (float)(currentTime - _lastEditorTime);
|
|
_lastEditorTime = currentTime;
|
|
|
|
_playbackTime += deltaTime;
|
|
float normalizedTime = (_playbackTime % _stateDuration) / _stateDuration;
|
|
|
|
_currentAnimator.Play(_currentState, 0, normalizedTime);
|
|
_currentAnimator.Update(0f);
|
|
|
|
// Blend from previous pose (if blending)
|
|
if (_blending)
|
|
{
|
|
_blendElapsedTime += deltaTime;
|
|
float t = Mathf.Clamp01(_blendElapsedTime / _blendDuration);
|
|
|
|
foreach (var bone in _animatorBones)
|
|
{
|
|
if (bone == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (_previousPosePos.TryGetValue(bone, out Vector3 prevPos))
|
|
{
|
|
bone.localPosition = Vector3.Lerp(prevPos, bone.localPosition, t);
|
|
}
|
|
|
|
if (_previousPoseRot.TryGetValue(bone, out Quaternion prevRot))
|
|
{
|
|
bone.localRotation = Quaternion.Slerp(prevRot, bone.localRotation, t);
|
|
}
|
|
}
|
|
|
|
if (t >= +_blendDuration)
|
|
{
|
|
_blending = false;
|
|
}
|
|
}
|
|
|
|
SceneView.RepaintAll();
|
|
|
|
if (_playbackTime >= _stateDuration)
|
|
{
|
|
_playbackTime = 0f;
|
|
_loopsRemaining--;
|
|
|
|
if (_loopsRemaining <= 0)
|
|
{
|
|
if (!_playingDefault)
|
|
{
|
|
StartDefaultState();
|
|
}
|
|
else
|
|
{
|
|
_loopsRemaining = Int32.MaxValue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Collect other states from the animation contoller.
|
|
/// </summary>
|
|
private void CollectOtherStates()
|
|
{
|
|
_otherStates.Clear();
|
|
if (_currentAnimationController == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (var layer in _currentAnimationController.layers)
|
|
{
|
|
foreach (var state in layer.stateMachine.states)
|
|
{
|
|
string stateName = state.state.name;
|
|
if (stateName != _defaultStateName)
|
|
{
|
|
_otherStates.Add(stateName);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
///
|
|
/// </summary>
|
|
private void StartDefaultState()
|
|
{
|
|
SetState(_defaultStateName);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the state of the animation controller.
|
|
/// </summary>
|
|
/// <param name="stateName">The name of the state to set.</param>
|
|
private void SetState(string stateName)
|
|
{
|
|
if (_currentAnimationController == null || _currentAnimator == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
CacheCurrentPose();
|
|
|
|
_currentState = stateName;
|
|
_playbackTime = 0f;
|
|
_blendElapsedTime = 0f;
|
|
_blending = true;
|
|
_loopsRemaining = stateName == _defaultStateName ? Int32.MaxValue : 1;
|
|
_playingDefault = stateName == _defaultStateName;
|
|
|
|
var sm = _currentAnimationController.layers[0].stateMachine;
|
|
foreach (var state in sm.states)
|
|
{
|
|
if (state.state.name == stateName && state.state.motion is AnimationClip clip)
|
|
{
|
|
_stateDuration = Mathf.Max(clip.length, 0.01f);
|
|
goto Found;
|
|
}
|
|
}
|
|
|
|
_stateDuration = _defaultClipDurationFallback;
|
|
|
|
Found:
|
|
_currentAnimator.Play(stateName, 0, 0f);
|
|
_currentAnimator.Update(0f);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Caches the current animation pose
|
|
/// </summary>
|
|
private void CacheCurrentPose()
|
|
{
|
|
_previousPosePos.Clear();
|
|
_previousPoseRot.Clear();
|
|
|
|
foreach (var bone in _animatorBones)
|
|
{
|
|
if (bone == null) continue;
|
|
_previousPosePos[bone] = bone.localPosition;
|
|
_previousPoseRot[bone] = bone.localRotation;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Processes the callback from the play mode state change.
|
|
/// </summary>
|
|
/// <param name="stateChange">The current PlayModeStateChange</param>
|
|
private void StateChange(PlayModeStateChange stateChange)
|
|
{
|
|
if (_useAutoSaveAndLoad && stateChange == PlayModeStateChange.ExitingEditMode || _useAutoSaveAndLoad && stateChange == PlayModeStateChange.ExitingPlayMode)
|
|
{
|
|
SerializedCharacter savedCharacter = CreateSerializedCharacter(_OUTPUT_MODEL_NAME);
|
|
Serializer serializer = new Serializer();
|
|
string serializedCharacter = serializer.Serialize(savedCharacter);
|
|
EditorPrefs.SetString(_AUTOSAVE_KEY, serializedCharacter);
|
|
}
|
|
|
|
if (_useAutoSaveAndLoad && stateChange == PlayModeStateChange.EnteredEditMode || _useAutoSaveAndLoad && stateChange == PlayModeStateChange.EnteredPlayMode)
|
|
{
|
|
_stateChange = stateChange;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
/// <inheritdoc cref="CreateGUI" />
|
|
public async void CreateGUI()
|
|
{
|
|
_loadingContent = true;
|
|
InitializeEditorWindow();
|
|
_root = rootVisualElement;
|
|
_root.Clear();
|
|
if (_editorStyle != null)
|
|
{
|
|
_root.styleSheets.Add(_editorStyle);
|
|
}
|
|
|
|
InitializeDatabase();
|
|
// if we still can't connect, something's gone wrong, don't keep building the GUI
|
|
if (_dbManager?.GetCurrentDbConnection() == null)
|
|
{
|
|
_loadingContent = false;
|
|
return;
|
|
}
|
|
|
|
// Maintains a linking to the model if the editor window is closed and re-opened.
|
|
_newModel = GameObject.Find(_OUTPUT_MODEL_NAME);
|
|
|
|
_previousPartSelections = new Dictionary<CharacterPartType, string>();
|
|
foreach (CharacterPartType type in Enum.GetValues(typeof(CharacterPartType)))
|
|
{
|
|
_previousPartSelections.Add(type, "None");
|
|
}
|
|
|
|
_partCountLabel = new Label("")
|
|
{
|
|
style =
|
|
{
|
|
unityTextAlign = TextAnchor.MiddleLeft
|
|
}
|
|
};
|
|
|
|
Image bannerImage = new Image
|
|
{
|
|
image = (Texture2D) Resources.Load("UI/T_SidekickTitle"),
|
|
scaleMode = ScaleMode.ScaleToFit,
|
|
};
|
|
VisualElement bannerLayout = new VisualElement
|
|
{
|
|
style =
|
|
{
|
|
backgroundColor = new Color(209f/256, 34f/256, 51f/256),
|
|
minHeight = 150,
|
|
paddingBottom = 5,
|
|
paddingTop = 5,
|
|
}
|
|
};
|
|
bannerLayout.Add(bannerImage);
|
|
_root.Add(bannerLayout);
|
|
|
|
_presetView = new ScrollView(ScrollViewMode.Vertical);
|
|
_partView = new ScrollView(ScrollViewMode.Vertical)
|
|
{
|
|
style =
|
|
{
|
|
display = DisplayStyle.None
|
|
}
|
|
};
|
|
_bodyShapeView = new ScrollView(ScrollViewMode.Vertical)
|
|
{
|
|
style =
|
|
{
|
|
display = DisplayStyle.None
|
|
}
|
|
};
|
|
|
|
_colorSelectionView = new ScrollView(ScrollViewMode.Vertical)
|
|
{
|
|
style =
|
|
{
|
|
display = DisplayStyle.None
|
|
}
|
|
};
|
|
|
|
// TODO: Uncomment when Decals added to the system.
|
|
// _decalSelectionView = new ScrollView(ScrollViewMode.Vertical)
|
|
// {
|
|
// style =
|
|
// {
|
|
// display = DisplayStyle.None
|
|
// }
|
|
// };
|
|
|
|
_optionSelectionView = new ScrollView(ScrollViewMode.Vertical)
|
|
{
|
|
style =
|
|
{
|
|
display = DisplayStyle.None
|
|
}
|
|
};
|
|
|
|
// TODO: Replace this tabbed menu code with TabView when 2023 LTS in the minimum supported version.
|
|
Toolbar tabBar = new Toolbar
|
|
{
|
|
style =
|
|
{
|
|
width = Length.Percent(100)
|
|
}
|
|
};
|
|
|
|
_bodyPresetTab = new ToolbarToggle
|
|
{
|
|
text = "Presets",
|
|
tooltip = "Create a character using preset combinations of parts, body types and colors"
|
|
};
|
|
|
|
_bodyPartsTab = new ToolbarToggle
|
|
{
|
|
text = "Parts",
|
|
tooltip = "Edit individual character parts on your character"
|
|
};
|
|
|
|
_bodyShapeTab = new ToolbarToggle
|
|
{
|
|
text = "Body",
|
|
tooltip = "Edit the body type, size, musculature of your character"
|
|
};
|
|
|
|
_colorSelectionTab = new ToolbarToggle
|
|
{
|
|
text = "Colors",
|
|
tooltip = "Edit individual colors of your character"
|
|
};
|
|
|
|
// TODO: Uncomment when Decals added to the system.
|
|
// _decalSelectionTab = new ToolbarToggle
|
|
// {
|
|
// text = "Decals",
|
|
// tooltip = "Edit the decals applied to your character"
|
|
// };
|
|
|
|
_optionTab = new ToolbarToggle
|
|
{
|
|
text = "Options",
|
|
tooltip = "Change the options of the tool"
|
|
};
|
|
|
|
tabBar.Add(_bodyPresetTab);
|
|
tabBar.Add(_bodyPartsTab);
|
|
tabBar.Add(_bodyShapeTab);
|
|
tabBar.Add(_colorSelectionTab);
|
|
// TODO: Uncomment when Decals added to the system.
|
|
// tabBar.Add(_decalSelectionTab);
|
|
tabBar.Add(_optionTab);
|
|
_root.Add(tabBar);
|
|
|
|
_bodyPresetTab.RegisterValueChangedCallback(
|
|
delegate
|
|
{
|
|
if (_currentTab != TabView.Preset && _bodyPresetTab.value)
|
|
{
|
|
SwitchToTab(TabView.Preset);
|
|
}
|
|
}
|
|
);
|
|
|
|
_bodyPartsTab.RegisterValueChangedCallback(
|
|
delegate
|
|
{
|
|
if (_currentTab != TabView.Parts && _bodyPartsTab.value)
|
|
{
|
|
SwitchToTab(TabView.Parts);
|
|
}
|
|
}
|
|
);
|
|
|
|
_bodyShapeTab.RegisterValueChangedCallback(
|
|
delegate
|
|
{
|
|
if (_currentTab != TabView.Body && _bodyShapeTab.value)
|
|
{
|
|
AddBodyShapeTabContent(_bodyShapeView);
|
|
SwitchToTab(TabView.Body);
|
|
}
|
|
}
|
|
);
|
|
|
|
_colorSelectionTab.RegisterValueChangedCallback(
|
|
delegate
|
|
{
|
|
if (_currentTab != TabView.Colors && _colorSelectionTab.value)
|
|
{
|
|
// always re-populate the color rows with the latest when switching tabs
|
|
if (_allColorRows.Count == 0)
|
|
{
|
|
PopulateColorRowsFromTextures();
|
|
}
|
|
else
|
|
{
|
|
PopulatePartColorRows();
|
|
RefreshVisibleColorRows();
|
|
}
|
|
|
|
SwitchToTab(TabView.Colors);
|
|
}
|
|
}
|
|
);
|
|
|
|
// TODO: Uncomment when Decals added to the system.
|
|
// _decalSelectionTab.RegisterValueChangedCallback(
|
|
// delegate
|
|
// {
|
|
// if (_currentTab != TabView.Decals && _decalSelectionTab.value)
|
|
// {
|
|
// SwitchToTab(TabView.Decals);
|
|
// }
|
|
// }
|
|
// );
|
|
|
|
_optionTab.RegisterValueChangedCallback(
|
|
delegate
|
|
{
|
|
if (_currentTab != TabView.Options && _optionTab.value)
|
|
{
|
|
SwitchToTab(TabView.Options);
|
|
}
|
|
// If currently on this tab, and button is toggled to "off", toggle back to "on"
|
|
else if (_currentTab == TabView.Options && !_optionTab.value)
|
|
{
|
|
_optionTab.value = true;
|
|
}
|
|
}
|
|
);
|
|
|
|
_root.Add(_presetView);
|
|
_root.Add(_partView);
|
|
_root.Add(_bodyShapeView);
|
|
_root.Add(_colorSelectionView);
|
|
// TODO: Uncomment when Decals added to the system.
|
|
// root.Add(_decalSelectionView);
|
|
_root.Add(_optionSelectionView);
|
|
|
|
AddOptionsTabContent(_optionSelectionView);
|
|
|
|
// set the default colour set (this will change on various input triggers, but we need it to not be null)
|
|
_currentColorSet = SidekickColorSet.GetDefault(_dbManager);
|
|
|
|
_partDictionary = new Dictionary<CharacterPartType, SkinnedMeshRenderer>();
|
|
_currentCharacter = new Dictionary<CharacterPartType, SidekickPart>();
|
|
|
|
_sidekickRuntime = new SidekickRuntime((GameObject) _baseModelField.value, (Material) _materialField.value, _currentAnimationController, _dbManager);
|
|
|
|
Label loadingLabel = new Label
|
|
{
|
|
text = "Loading Content..",
|
|
style =
|
|
{
|
|
fontSize = 20,
|
|
unityTextAlign = new StyleEnum<TextAnchor>(TextAnchor.MiddleCenter),
|
|
marginTop = 20
|
|
}
|
|
};
|
|
|
|
_presetView.Add(loadingLabel);
|
|
|
|
_loadingImage = new Image
|
|
{
|
|
image = (Texture2D) Resources.Load("UI/T_LoadingCircle"),
|
|
scaleMode = ScaleMode.ScaleToFit,
|
|
style =
|
|
{
|
|
width = 28,
|
|
height = 28,
|
|
unityTextAlign = new StyleEnum<TextAnchor>(TextAnchor.MiddleCenter),
|
|
alignContent = new StyleEnum<Align>(Align.Center),
|
|
alignSelf = new StyleEnum<Align>(Align.Center)
|
|
}
|
|
};
|
|
|
|
_presetView.Add(_loadingImage);
|
|
|
|
_bodyPresetTab.value = true;
|
|
SwitchToTab(TabView.Preset);
|
|
|
|
VisualElement saveLoadButtons = new VisualElement
|
|
{
|
|
style =
|
|
{
|
|
flexDirection = new StyleEnum<FlexDirection>(FlexDirection.Row),
|
|
width = Length.Percent(100),
|
|
alignContent = new StyleEnum<Align>(Align.FlexStart),
|
|
alignItems = new StyleEnum<Align>(Align.FlexStart),
|
|
flexWrap = new StyleEnum<Wrap>(Wrap.Wrap),
|
|
justifyContent = new StyleEnum<Justify>(Justify.SpaceBetween),
|
|
minHeight = 30,
|
|
marginTop = 20
|
|
}
|
|
};
|
|
|
|
_root.Add(saveLoadButtons);
|
|
|
|
Button loadCharacterButton = new Button(LoadCharacter)
|
|
{
|
|
text = "Load Character",
|
|
style =
|
|
{
|
|
minHeight = 30,
|
|
width = Length.Percent(48)
|
|
}
|
|
};
|
|
saveLoadButtons.Add(loadCharacterButton);
|
|
|
|
Button saveCharacterButton = new Button(SaveCharacter)
|
|
{
|
|
text = "Save Character",
|
|
style =
|
|
{
|
|
minHeight = 30,
|
|
width = Length.Percent(48)
|
|
}
|
|
};
|
|
|
|
saveLoadButtons.Add(saveCharacterButton);
|
|
|
|
Button createCharacterButton = new Button(CreateCharacterPrefab)
|
|
{
|
|
text = "Export Character as Prefab",
|
|
style =
|
|
{
|
|
minHeight = 50,
|
|
marginTop = 5
|
|
}
|
|
};
|
|
_root.Add(createCharacterButton);
|
|
|
|
// Populate the data in an async process, if not already populated
|
|
try
|
|
{
|
|
await Task.Run(
|
|
async () =>
|
|
{
|
|
await SidekickRuntime.PopulateToolData(_sidekickRuntime);
|
|
_callbackQueue.Enqueue(AddAllTabContent);
|
|
if (_useAutoSaveAndLoad && _stateChange == PlayModeStateChange.EnteredEditMode
|
|
|| _useAutoSaveAndLoad && _stateChange == PlayModeStateChange.EnteredPlayMode)
|
|
{
|
|
_callbackQueue.Enqueue(ReloadCharacterFromStateChange);
|
|
}
|
|
}
|
|
);
|
|
}
|
|
catch
|
|
{
|
|
Debug.LogWarning("Failed to load tool data. Please try again.\nPlease note that data loading may take some time.");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reloads the character based on auto saved character
|
|
/// </summary>
|
|
private void ReloadCharacterFromStateChange()
|
|
{
|
|
if (_stateChange == PlayModeStateChange.EnteredEditMode)
|
|
{
|
|
GameObject existingModel = GameObject.Find(_OUTPUT_MODEL_NAME);
|
|
while (existingModel != null)
|
|
{
|
|
GameObject.DestroyImmediate(existingModel);
|
|
existingModel = GameObject.Find(_OUTPUT_MODEL_NAME);
|
|
}
|
|
}
|
|
|
|
string serializedCharacterString = EditorPrefs.GetString(_AUTOSAVE_KEY, null);
|
|
|
|
if (!string.IsNullOrEmpty(serializedCharacterString))
|
|
{
|
|
Deserializer deserializer = new Deserializer();
|
|
SerializedCharacter serializedCharacter = deserializer.Deserialize<SerializedCharacter>(serializedCharacterString);
|
|
LoadSerializedCharacter(serializedCharacter, _showAllColourProperties);
|
|
}
|
|
|
|
_stateChange = PlayModeStateChange.ExitingEditMode;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds the UI content to all tabs (excluding Options tab as it is already populated)
|
|
/// </summary>
|
|
private void AddAllTabContent()
|
|
{
|
|
if (_sidekickRuntime.PartCount < 15)
|
|
{
|
|
if (EditorUtility.DisplayDialog(
|
|
"Unable to strat Sidekicks Tool",
|
|
"There are not enough Sidekicks parts currently in your project. The tool is unable to start without the requisite parts",
|
|
"Ok"
|
|
))
|
|
{
|
|
Close();
|
|
}
|
|
|
|
}
|
|
else {
|
|
_allPartsLibrary = _sidekickRuntime.AllPartsLibrary;
|
|
_allParts = SidekickPart.GetAll(_dbManager);
|
|
|
|
_allSpecies = SidekickSpecies.GetAll(_dbManager);
|
|
_currentSpecies = _allSpecies[0];
|
|
_sidekickRuntime.CurrentSpecies = _currentSpecies;
|
|
|
|
_partCountLabel.text = _sidekickRuntime.PartCount + _PART_COUNT_BODY;
|
|
_partOutfitMap = _sidekickRuntime.PartOutfitMap;
|
|
|
|
AddBodyShapeTabContent(_bodyShapeView);
|
|
AddColorTabContent(_colorSelectionView);
|
|
// TODO: Uncomment when Decals added to the system.
|
|
// _decalSelectionView.Add(new Label("Decal selections will go here."));
|
|
|
|
UpdatePartUVData();
|
|
|
|
PopulatePartUI();
|
|
PopulatePresetUI();
|
|
|
|
if (_currentSpecies == null)
|
|
{
|
|
_currentSpecies = _currentSpecies = _allSpecies.FirstOrDefault(species => species.Name == _speciesField.value);
|
|
}
|
|
|
|
if (_useAutoSaveAndLoad)
|
|
{
|
|
string serializedCharacterString = EditorPrefs.GetString(_AUTOSAVE_KEY, null);
|
|
|
|
if (!string.IsNullOrEmpty(serializedCharacterString))
|
|
{
|
|
try
|
|
{
|
|
Deserializer deserializer = new Deserializer();
|
|
SerializedCharacter serializedCharacter = deserializer.Deserialize<SerializedCharacter>(serializedCharacterString);
|
|
LoadSerializedCharacter(serializedCharacter, _showAllColourProperties);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
EditorUtility.DisplayDialog(
|
|
"Something Went Wrong",
|
|
"Unable to load saved character.",
|
|
"OK"
|
|
);
|
|
Debug.LogWarning(ex);
|
|
_useAutoSaveAndLoad = false;
|
|
EditorPrefs.SetBool(_AUTOSAVE_STATE, _useAutoSaveAndLoad);
|
|
}
|
|
}
|
|
}
|
|
_loadingContent = false;
|
|
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes all the database setup, and provides the status label reference for users
|
|
/// </summary>
|
|
private void InitializeDatabase()
|
|
{
|
|
// TODO: always reinstantiate instead?
|
|
if (_dbManager != null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_dbManager = new DatabaseManager();
|
|
_dbManager.GetDbConnection(true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes the editor window.
|
|
/// </summary>
|
|
private void InitializeEditorWindow()
|
|
{
|
|
_editorStyle = Resources.Load<StyleSheet>("Styles/EditorStyles");
|
|
_openWindowOnStart = EditorPrefs.GetBool(_AUTO_OPEN_STATE, true);
|
|
_useAutoSaveAndLoad = EditorPrefs.GetBool(_AUTOSAVE_STATE, false);
|
|
_showMissingPartsPopup = EditorPrefs.GetBool(_AUTOSAVE_MISSING_PARTS, false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds the contents and change listeners for the body shape tab.
|
|
/// </summary>
|
|
/// <param name="view">The tabview to add the content to.</param>
|
|
private void AddBodyShapeTabContent(ScrollView view)
|
|
{
|
|
view.Clear();
|
|
|
|
_bodyTypeSlider = new Slider("Body Type", -100, 100)
|
|
{
|
|
value = _bodyTypeBlendValue,
|
|
style =
|
|
{
|
|
maxWidth = new StyleLength(StyleKeyword.Auto)
|
|
},
|
|
showInputField = true,
|
|
tooltip = "Blend the body type of the character between masculine and feminine"
|
|
};
|
|
|
|
VisualElement bodyTypeLabels = new VisualElement
|
|
{
|
|
style =
|
|
{
|
|
flexDirection = new StyleEnum<FlexDirection>(FlexDirection.Row),
|
|
width = Length.Percent(100),
|
|
paddingLeft = 155
|
|
}
|
|
};
|
|
|
|
Label labelMasculine = new Label("Masculine")
|
|
{
|
|
style =
|
|
{
|
|
position = new StyleEnum<Position>(Position.Absolute),
|
|
left = 155
|
|
}
|
|
};
|
|
bodyTypeLabels.Add(labelMasculine);
|
|
|
|
Label labelFeminine = new Label("Feminine")
|
|
{
|
|
style =
|
|
{
|
|
position = new StyleEnum<Position>(Position.Absolute),
|
|
right = 58
|
|
}
|
|
};
|
|
bodyTypeLabels.Add(labelFeminine);
|
|
|
|
_bodySizeSlider = new Slider("Body Size", -100, 100)
|
|
{
|
|
value = _bodySizeSkinnyBlendValue > 0 ? -_bodySizeSkinnyBlendValue : _bodySizeHeavyBlendValue,
|
|
style =
|
|
{
|
|
maxWidth = new StyleLength(StyleKeyword.Auto),
|
|
marginTop = 30
|
|
},
|
|
showInputField = true,
|
|
tooltip = "Blend the body size of the character between slim and heavy"
|
|
};
|
|
|
|
VisualElement bodySizeLabels = new VisualElement
|
|
{
|
|
style =
|
|
{
|
|
flexDirection = new StyleEnum<FlexDirection>(FlexDirection.Row),
|
|
width = Length.Percent(100),
|
|
paddingLeft = 155
|
|
}
|
|
};
|
|
|
|
Label labelSlim = new Label("Slim")
|
|
{
|
|
style =
|
|
{
|
|
position = new StyleEnum<Position>(Position.Absolute),
|
|
left = 155
|
|
}
|
|
};
|
|
bodySizeLabels.Add(labelSlim);
|
|
|
|
Label labelHeavy = new Label("Heavy")
|
|
{
|
|
style =
|
|
{
|
|
position = new StyleEnum<Position>(Position.Absolute),
|
|
right = 58
|
|
}
|
|
};
|
|
bodySizeLabels.Add(labelHeavy);
|
|
|
|
_musclesSlider = new Slider("Musculature", -100, 100)
|
|
{
|
|
value = _musclesBlendValue,
|
|
style =
|
|
{
|
|
maxWidth = new StyleLength(StyleKeyword.Auto),
|
|
marginTop = 30
|
|
},
|
|
showInputField = true,
|
|
tooltip = "Blend the musculature of the character between lean and muscular"
|
|
};
|
|
|
|
VisualElement muscleLabels = new VisualElement
|
|
{
|
|
style =
|
|
{
|
|
flexDirection = new StyleEnum<FlexDirection>(FlexDirection.Row),
|
|
width = Length.Percent(100),
|
|
height = 20,
|
|
paddingLeft = 155
|
|
}
|
|
};
|
|
|
|
Label labelLean = new Label("Lean")
|
|
{
|
|
style =
|
|
{
|
|
position = new StyleEnum<Position>(Position.Absolute),
|
|
left = 155
|
|
}
|
|
};
|
|
muscleLabels.Add(labelLean);
|
|
|
|
Label labelBulk = new Label("Muscular")
|
|
{
|
|
style =
|
|
{
|
|
position = new StyleEnum<Position>(Position.Absolute),
|
|
right = 58
|
|
}
|
|
};
|
|
muscleLabels.Add(labelBulk);
|
|
|
|
view.Add(_bodyTypeSlider);
|
|
view.Add(bodyTypeLabels);
|
|
view.Add(_bodySizeSlider);
|
|
view.Add(bodySizeLabels);
|
|
view.Add(_musclesSlider);
|
|
view.Add(muscleLabels);
|
|
|
|
_bodyTypeSlider.RegisterValueChangedCallback(
|
|
evt =>
|
|
{
|
|
_bodyTypeBlendValue = evt.newValue;
|
|
_sidekickRuntime.BodyTypeBlendValue = evt.newValue;
|
|
|
|
string body = "None";
|
|
if (_partSelectionDictionary.TryGetValue(CharacterPartType.Torso, out PartTypeControls bodySelection))
|
|
{
|
|
body = bodySelection.PartDropdown.value;
|
|
}
|
|
|
|
if (_partSelectionDictionary.TryGetValue(CharacterPartType.Wrap, out PartTypeControls wrapSelection))
|
|
{
|
|
if (body != "None" && _requiresWrap && _bodyTypeBlendValue > 0)
|
|
{
|
|
wrapSelection.PartDropdown.SetEnabled(true);
|
|
wrapSelection.RandomisePartDropdownValue();
|
|
}
|
|
else
|
|
{
|
|
wrapSelection.PartDropdown.SetEnabled(false);
|
|
wrapSelection.SetPartDropdownValue(null);
|
|
_currentCharacter.Remove(CharacterPartType.Wrap);
|
|
}
|
|
}
|
|
|
|
if (_newModel == null)
|
|
{
|
|
_newModel = GenerateCharacter(false, true);
|
|
UpdatePartUVData();
|
|
}
|
|
|
|
_sidekickRuntime.UpdateBlendShapes(_newModel);
|
|
_sidekickRuntime.ProcessRigMovementOnBlendShapeChange(SidekickBlendShapeRigMovement.GetAllForProcessing(_dbManager));
|
|
_sidekickRuntime.ProcessBoneMovement(_newModel);
|
|
}
|
|
);
|
|
|
|
_bodySizeSlider.RegisterValueChangedCallback(
|
|
evt =>
|
|
{
|
|
float newValue = evt.newValue;
|
|
if (newValue > 0)
|
|
{
|
|
_bodySizeHeavyBlendValue = newValue;
|
|
_bodySizeSkinnyBlendValue = 0;
|
|
_sidekickRuntime.BodySizeHeavyBlendValue = newValue;
|
|
_sidekickRuntime.BodySizeSkinnyBlendValue = 0;
|
|
}
|
|
else if (newValue < 0)
|
|
{
|
|
_bodySizeHeavyBlendValue = 0;
|
|
_bodySizeSkinnyBlendValue = -newValue;
|
|
_sidekickRuntime.BodySizeHeavyBlendValue = 0;
|
|
_sidekickRuntime.BodySizeSkinnyBlendValue = -newValue;
|
|
}
|
|
else
|
|
{
|
|
_bodySizeHeavyBlendValue = 0;
|
|
_bodySizeSkinnyBlendValue = 0;
|
|
_sidekickRuntime.BodySizeHeavyBlendValue = 0;
|
|
_sidekickRuntime.BodySizeSkinnyBlendValue = 0;
|
|
}
|
|
|
|
if (_newModel == null)
|
|
{
|
|
_newModel = GenerateCharacter(false, true);
|
|
UpdatePartUVData();
|
|
}
|
|
|
|
_sidekickRuntime.UpdateBlendShapes(_newModel);
|
|
_sidekickRuntime.ProcessRigMovementOnBlendShapeChange(SidekickBlendShapeRigMovement.GetAllForProcessing(_dbManager));
|
|
_sidekickRuntime.ProcessBoneMovement(_newModel);
|
|
}
|
|
);
|
|
|
|
_musclesSlider.RegisterValueChangedCallback(
|
|
evt =>
|
|
{
|
|
_musclesBlendValue = evt.newValue;
|
|
_sidekickRuntime.MusclesBlendValue = evt.newValue;
|
|
|
|
if (_newModel == null)
|
|
{
|
|
_newModel = GenerateCharacter(false, true);
|
|
UpdatePartUVData();
|
|
}
|
|
|
|
_sidekickRuntime.UpdateBlendShapes(_newModel);
|
|
_sidekickRuntime.ProcessRigMovementOnBlendShapeChange(SidekickBlendShapeRigMovement.GetAllForProcessing(_dbManager));
|
|
_sidekickRuntime.ProcessBoneMovement(_newModel);
|
|
}
|
|
);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds the content to the Color tab.
|
|
/// </summary>
|
|
/// <param name="view">The view to add the content to.</param>
|
|
private void AddColorTabContent(ScrollView view)
|
|
{
|
|
_colorSetsDropdown = new DropdownField
|
|
{
|
|
style =
|
|
{
|
|
maxWidth = Length.Percent(65)
|
|
}
|
|
};
|
|
|
|
Label filterPartsLabel = new Label("Filter - Parts")
|
|
{
|
|
tooltip = "Filter the displayed colors to focus on specific areas of the character and reduce the number of properties"
|
|
};
|
|
view.Add(filterPartsLabel);
|
|
DropdownField partTypeDropdown = new DropdownField();
|
|
string[] colorPartTypes = Enum.GetNames(typeof(ColorPartType));
|
|
// Enum names can't have spaces so we add in the space manually for display.
|
|
for (int i = 0; i < colorPartTypes.Length; i++)
|
|
{
|
|
colorPartTypes[i] = StringUtils.AddSpacesBeforeCapitalLetters(colorPartTypes[i]);
|
|
}
|
|
|
|
partTypeDropdown.choices = colorPartTypes.ToList();
|
|
partTypeDropdown.value = colorPartTypes[0];
|
|
view.Add(partTypeDropdown);
|
|
|
|
partTypeDropdown.RegisterValueChangedCallback(
|
|
(evt) =>
|
|
{
|
|
if (Enum.TryParse(typeof(ColorPartType), evt.newValue.Replace(" ", ""), out object newType))
|
|
{
|
|
_currentPartType = (ColorPartType) newType;
|
|
_colorSetsDropdown.value = "Custom";
|
|
}
|
|
|
|
PopulatePartColorRows();
|
|
RefreshVisibleColorRows();
|
|
}
|
|
);
|
|
|
|
_currentPartType = ColorPartType.AllParts;
|
|
|
|
// TODO: Hidden due to early access, enable when feature complete
|
|
// Label colorSetsLabel = new Label("Color Sets");
|
|
// view.Add(colorSetsLabel);
|
|
//
|
|
// VisualElement colorSetsRow = new VisualElement
|
|
// {
|
|
// style =
|
|
// {
|
|
// flexDirection = new StyleEnum<FlexDirection>(FlexDirection.Row),
|
|
// width = Length.Percent(100),
|
|
// alignContent = new StyleEnum<Align>(Align.FlexStart),
|
|
// alignItems = new StyleEnum<Align>(Align.FlexStart),
|
|
// flexWrap = new StyleEnum<Wrap>(Wrap.Wrap),
|
|
// justifyContent = new StyleEnum<Justify>(Justify.SpaceBetween),
|
|
// marginBottom = 10
|
|
// }
|
|
// };
|
|
|
|
UpdateVisibleColorSets();
|
|
|
|
// TODO: Hidden due to early access, enable when feature complete
|
|
// colorSetsRow.Add(_colorSetsDropdown);
|
|
//
|
|
// _colorSetsDropdown.RegisterValueChangedCallback(
|
|
// evt =>
|
|
// {
|
|
// if (evt.newValue != "Custom")
|
|
// {
|
|
// SidekickColorSet newSet = _visibleColorSets.First(set => set.Name == evt.newValue);
|
|
// List<SidekickColorRow> newRows = SidekickColorRow.GetAllBySet(_dbManager, newSet);
|
|
// if (_currentPartType == ColorPartType.AllParts && _allColorRows.All(row => row.IsLocked == false))
|
|
// {
|
|
// _currentColorSet = newSet;
|
|
// _allColorRows = newRows;
|
|
// if (_allColorRows.Count == 0)
|
|
// {
|
|
// PopulateColorRowsFromTextures();
|
|
// }
|
|
// else
|
|
// {
|
|
// PopulatePartColorRows();
|
|
// RefreshVisibleColorRows();
|
|
// }
|
|
// }
|
|
// else
|
|
// {
|
|
// foreach (SidekickColorRow row in _visibleColorRows)
|
|
// {
|
|
// SidekickColorRow newRow = newRows.FirstOrDefault(r => r.ColorProperty.ID == row.ColorProperty.ID);
|
|
// if (newRow != null)
|
|
// {
|
|
// row.NiceColor = newRow.NiceColor;
|
|
// row.NiceMetallic = newRow.NiceMetallic;
|
|
// row.NiceSmoothness = newRow.NiceSmoothness;
|
|
// row.NiceReflection = newRow.NiceReflection;
|
|
// row.NiceEmission = newRow.NiceEmission;
|
|
// row.NiceOpacity = newRow.NiceOpacity;
|
|
// }
|
|
// }
|
|
//
|
|
// RefreshVisibleColorRows();
|
|
// }
|
|
//
|
|
// UpdateAllVisibleColors();
|
|
// }
|
|
// }
|
|
// );
|
|
//
|
|
// Button previousSetButton = new Button(
|
|
// () =>
|
|
// {
|
|
// if (_colorSetsDropdown.index > 0)
|
|
// {
|
|
// _colorSetsDropdown.index -= 1;
|
|
// }
|
|
// }
|
|
// )
|
|
// {
|
|
// tooltip = "Previous Color Set"
|
|
// };
|
|
//
|
|
// previousSetButton.Add(
|
|
// new Image
|
|
// {
|
|
// image = EditorGUIUtility.IconContent("tab_prev", "|Previous Color Set").image,
|
|
// scaleMode = ScaleMode.ScaleToFit
|
|
// }
|
|
// );
|
|
//
|
|
// colorSetsRow.Add(previousSetButton);
|
|
// Button nextSetButton = new Button(
|
|
// () =>
|
|
// {
|
|
// if (_colorSetsDropdown.index < _colorSetsDropdown.choices.Count - 1)
|
|
// {
|
|
// _colorSetsDropdown.index += 1;
|
|
// }
|
|
// }
|
|
// )
|
|
// {
|
|
// tooltip = "Next Color Set"
|
|
// };
|
|
//
|
|
// nextSetButton.Add(
|
|
// new Image
|
|
// {
|
|
// image = EditorGUIUtility.IconContent("tab_next", "|Next Color Set").image,
|
|
// scaleMode = ScaleMode.ScaleToFit
|
|
// }
|
|
// );
|
|
//
|
|
// colorSetsRow.Add(nextSetButton);
|
|
// Button resetSetButton = new Button(ResetColorSet)
|
|
// {
|
|
// tooltip = "Reset Color Set from Disk"
|
|
// };
|
|
// resetSetButton.Add(
|
|
// new Image
|
|
// {
|
|
// image = EditorGUIUtility.IconContent("Refresh", "|Reset Color Set From Disk").image,
|
|
// scaleMode = ScaleMode.ScaleToFit
|
|
// }
|
|
// );
|
|
//
|
|
// colorSetsRow.Add(resetSetButton);
|
|
// Button newSetButton = new Button(ShowCreateNewColorSet)
|
|
// {
|
|
// tooltip = "Create New Color Set"
|
|
// };
|
|
// newSetButton.Add(
|
|
// new Image
|
|
// {
|
|
// image = EditorGUIUtility.IconContent("Toolbar Plus", "|Create New Color Set").image,
|
|
// scaleMode = ScaleMode.ScaleToFit
|
|
// }
|
|
// );
|
|
//
|
|
// colorSetsRow.Add(newSetButton);
|
|
// Button deleteSetButton = new Button(DeleteColorSet)
|
|
// {
|
|
// tooltip = "Delete Color Set"
|
|
// };
|
|
// deleteSetButton.Add(
|
|
// new Image
|
|
// {
|
|
// image = EditorGUIUtility.IconContent("close", "|Delete Color Set").image,
|
|
// scaleMode = ScaleMode.ScaleToFit
|
|
// }
|
|
// );
|
|
//
|
|
// colorSetsRow.Add(deleteSetButton);
|
|
// Button saveSetButton = new Button(SaveColorSet)
|
|
// {
|
|
// tooltip = "Save Color Set"
|
|
// };
|
|
// saveSetButton.Add(
|
|
// new Image
|
|
// {
|
|
// image = EditorGUIUtility.IconContent("SaveAs", "|Save Color Set").image,
|
|
// scaleMode = ScaleMode.ScaleToFit
|
|
// }
|
|
// );
|
|
//
|
|
// colorSetsRow.Add(saveSetButton);
|
|
// view.Add(colorSetsRow);
|
|
//
|
|
// _newSetNameContainer = new VisualElement
|
|
// {
|
|
// style =
|
|
// {
|
|
// flexDirection = new StyleEnum<FlexDirection>(FlexDirection.Row),
|
|
// width = Length.Percent(100),
|
|
// alignContent = new StyleEnum<Align>(Align.FlexStart),
|
|
// alignItems = new StyleEnum<Align>(Align.FlexStart),
|
|
// flexWrap = new StyleEnum<Wrap>(Wrap.Wrap),
|
|
// marginBottom = 10,
|
|
// display = DisplayStyle.None
|
|
// }
|
|
// };
|
|
//
|
|
// TextField newNameField = new TextField("New Set Name")
|
|
// {
|
|
// style =
|
|
// {
|
|
// minWidth = Length.Percent(70)
|
|
// }
|
|
// };
|
|
// _newSetNameContainer.Add(newNameField);
|
|
//
|
|
// Button newSetCreateButton = new Button(
|
|
// () =>
|
|
// {
|
|
// CreateNewColorSet(newNameField.value);
|
|
// List<string> choices = _colorSetsDropdown.choices;
|
|
// choices.Add(newNameField.value);
|
|
// _colorSetsDropdown.choices = choices;
|
|
// _colorSetsDropdown.value = newNameField.value;
|
|
// _newSetNameContainer.style.display = DisplayStyle.None;
|
|
// }
|
|
// )
|
|
// {
|
|
// text = "Create Set"
|
|
// };
|
|
//
|
|
// newNameField.RegisterValueChangedCallback(
|
|
// evt =>
|
|
// {
|
|
// newSetCreateButton.SetEnabled(!SidekickColorSet.DoesNameExist(_dbManager, evt.newValue));
|
|
// }
|
|
// );
|
|
//
|
|
// _newSetNameContainer.Add(newSetCreateButton);
|
|
// view.Add(_newSetNameContainer);
|
|
//
|
|
// VisualElement allRow = new VisualElement();
|
|
// allRow.AddToClassList("colorSelectionRow");
|
|
//
|
|
// Label allItemsLabel = new Label("All");
|
|
// allItemsLabel.AddToClassList("colorSelectionRowLabel");
|
|
// allRow.Add(allItemsLabel);
|
|
//
|
|
// VisualElement allRowContent = new VisualElement();
|
|
// allRowContent.AddToClassList("colorSelectionRowContent");
|
|
// allRow.Add(allRowContent);
|
|
//
|
|
// Button btnAllLock = new Button
|
|
// {
|
|
// style =
|
|
// {
|
|
// left = 0
|
|
// }
|
|
// };
|
|
// allRowContent.Add(btnAllLock);
|
|
// Image lockButtonImage = new Image
|
|
// {
|
|
// image = _currentGlobalLockStatus
|
|
// ? EditorGUIUtility.IconContent("Locked").image
|
|
// : EditorGUIUtility.IconContent("Unlocked").image,
|
|
// scaleMode = ScaleMode.ScaleToFit
|
|
// };
|
|
//
|
|
// btnAllLock.Add(lockButtonImage);
|
|
//
|
|
// btnAllLock.clickable.clicked += () =>
|
|
// {
|
|
// foreach (SidekickColorRow colorRow in _visibleColorRows)
|
|
// {
|
|
// colorRow.IsLocked = !_currentGlobalLockStatus;
|
|
// if (colorRow.ButtonImage != null)
|
|
// {
|
|
// colorRow.ButtonImage.image =
|
|
// colorRow.IsLocked
|
|
// ? EditorGUIUtility.IconContent("Locked").image
|
|
// : EditorGUIUtility.IconContent("Unlocked").image;
|
|
// }
|
|
// }
|
|
//
|
|
// _currentGlobalLockStatus = !_currentGlobalLockStatus;
|
|
// lockButtonImage.image = _currentGlobalLockStatus
|
|
// ? EditorGUIUtility.IconContent("Locked").image
|
|
// : EditorGUIUtility.IconContent("Unlocked").image;
|
|
// };
|
|
//
|
|
// TODO: Uncomment once all colors options are re-enabled
|
|
// Button randomAllButton = new Button
|
|
// {
|
|
// text = "R",
|
|
// style =
|
|
// {
|
|
// right = 0
|
|
// }
|
|
// };
|
|
// allRowContent.Add(randomAllButton);
|
|
//
|
|
// view.Add(allRow);
|
|
|
|
_colorSelectionRowView = new VisualElement
|
|
{
|
|
style =
|
|
{
|
|
width = Length.Percent(100)
|
|
}
|
|
};
|
|
|
|
view.Add(_colorSelectionRowView);
|
|
|
|
UpdateColorTabContent();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds the content to the options tab.
|
|
/// </summary>
|
|
/// <param name="view">The view to add the content to.</param>
|
|
private void AddOptionsTabContent(VisualElement view)
|
|
{
|
|
Label baseAssetLabel = new Label
|
|
{
|
|
style =
|
|
{
|
|
marginTop = 5,
|
|
marginLeft = 12,
|
|
unityFontStyleAndWeight = new StyleEnum<FontStyle>(FontStyle.Bold)
|
|
},
|
|
text = "Base Assets",
|
|
tooltip = "These assets are used to construct the character"
|
|
};
|
|
|
|
view.Add(baseAssetLabel);
|
|
|
|
_baseModelField = new ObjectField
|
|
{
|
|
style =
|
|
{
|
|
marginLeft = 15,
|
|
marginRight = 15
|
|
},
|
|
tooltip = "The rigged character model used when constructing a character",
|
|
objectType = typeof(GameObject),
|
|
label = "Model"
|
|
};
|
|
|
|
_baseModelField.RegisterCallback<ChangeEvent<Object>>(
|
|
changeEvent =>
|
|
{
|
|
// TODO: Check the model has a minimum of 1 SkinnedMeshRenderer as a child.
|
|
}
|
|
);
|
|
view.Add(_baseModelField);
|
|
|
|
_baseModelField.value = Resources.Load<GameObject>(_BASE_MESH_NAME);
|
|
|
|
_materialField = new ObjectField
|
|
{
|
|
tooltip = "The material used when constructing a character",
|
|
objectType = typeof(Material),
|
|
label = "Material",
|
|
style =
|
|
{
|
|
marginLeft = 15,
|
|
marginRight = 15
|
|
}
|
|
};
|
|
|
|
view.Add(_materialField);
|
|
|
|
_materialField.value = Resources.Load<Material>(_BASE_MATERIAL_NAME);
|
|
|
|
_animationField = new ObjectField
|
|
{
|
|
tooltip = "The animation controller applied when constructing a character",
|
|
objectType = typeof(AnimatorController),
|
|
label = "Animation Controller",
|
|
value = _currentAnimationController,
|
|
style =
|
|
{
|
|
marginLeft = 15,
|
|
marginRight = 15
|
|
}
|
|
};
|
|
|
|
view.Add(_animationField);
|
|
|
|
_animationField.RegisterValueChangedCallback(
|
|
evt =>
|
|
{
|
|
_currentAnimationController = (AnimatorController) evt.newValue;
|
|
_sidekickRuntime.CurrentAnimationController = _currentAnimationController;
|
|
}
|
|
);
|
|
|
|
VisualElement updateLibraryLayout = new VisualElement
|
|
{
|
|
style =
|
|
{
|
|
minHeight = 20,
|
|
display = DisplayStyle.Flex,
|
|
flexDirection = FlexDirection.Row,
|
|
marginBottom = 2,
|
|
marginTop = 10,
|
|
marginLeft = 15,
|
|
marginRight = 2
|
|
}
|
|
};
|
|
|
|
Button uploadLibraryButton = new Button()
|
|
{
|
|
text = "Update Part Library",
|
|
tooltip = "Re-scans the project folders to update the parts list"
|
|
};
|
|
|
|
uploadLibraryButton.clickable.clicked += async delegate
|
|
{
|
|
CreateGUI();
|
|
await Task.Run(
|
|
async () =>
|
|
{
|
|
await SidekickRuntime.PopulateToolData(_sidekickRuntime);
|
|
_callbackQueue.Enqueue(AddAllTabContent);
|
|
}
|
|
);
|
|
|
|
};
|
|
|
|
updateLibraryLayout.Add(uploadLibraryButton);
|
|
updateLibraryLayout.Add(_partCountLabel);
|
|
view.Add(updateLibraryLayout);
|
|
|
|
Label prefabOptions = new Label
|
|
{
|
|
style =
|
|
{
|
|
marginTop = 5,
|
|
marginLeft = 12,
|
|
unityFontStyleAndWeight = new StyleEnum<FontStyle>(FontStyle.Bold)
|
|
},
|
|
text = "Prefab Options",
|
|
tooltip = "Options for how prefabs are created"
|
|
};
|
|
|
|
view.Add(prefabOptions);
|
|
|
|
Toggle combineToggle = new Toggle("Combine Character Meshes")
|
|
{
|
|
value = _combineMeshes,
|
|
style =
|
|
{
|
|
marginTop = 10,
|
|
marginLeft = 15
|
|
},
|
|
tooltip = "Whether or not to bake all the meshes down to a single mesh in the output model."
|
|
};
|
|
|
|
combineToggle.RegisterValueChangedCallback(
|
|
evt =>
|
|
{
|
|
_combineMeshes = evt.newValue;
|
|
}
|
|
);
|
|
|
|
view.Add(combineToggle);
|
|
|
|
Toggle bakeBlendsToggle = new Toggle("Combine Body Blend Shapes")
|
|
{
|
|
value = _bakeBlends,
|
|
style =
|
|
{
|
|
marginTop = 10,
|
|
marginLeft = 15
|
|
},
|
|
tooltip = "Whether or not to bake the body blend shapes into the mesh in the output model."
|
|
};
|
|
|
|
bakeBlendsToggle.RegisterValueChangedCallback(
|
|
evt =>
|
|
{
|
|
_bakeBlends = evt.newValue;
|
|
}
|
|
);
|
|
|
|
view.Add(bakeBlendsToggle);
|
|
|
|
Label toolOptions = new Label
|
|
{
|
|
style =
|
|
{
|
|
marginTop = 5,
|
|
marginLeft = 12,
|
|
unityFontStyleAndWeight = new StyleEnum<FontStyle>(FontStyle.Bold)
|
|
},
|
|
text = "Tool Options",
|
|
tooltip = "Options for how the tool behaves"
|
|
};
|
|
|
|
view.Add(toolOptions);
|
|
|
|
_previewToggle = new Toggle("Auto Build Model")
|
|
{
|
|
value = true,
|
|
style =
|
|
{
|
|
marginTop = 10,
|
|
marginLeft = 15
|
|
}
|
|
};
|
|
|
|
//view.Add(_previewToggle);
|
|
|
|
Toggle filterColorsToggle = new Toggle("Show all color properties")
|
|
{
|
|
value = _showAllColourProperties,
|
|
style =
|
|
{
|
|
marginTop = 10,
|
|
marginLeft = 15
|
|
},
|
|
tooltip = "Display all color properties in the color tab rather than limited to only what the current character is using"
|
|
};
|
|
|
|
filterColorsToggle.RegisterValueChangedCallback(
|
|
evt =>
|
|
{
|
|
_showAllColourProperties = evt.newValue;
|
|
UpdateVisibleColorSets();
|
|
UpdateColorTabContent();
|
|
}
|
|
);
|
|
|
|
view.Add(filterColorsToggle);
|
|
|
|
Toggle autoOpenToggle = new Toggle("Open tool on startup")
|
|
{
|
|
value = _openWindowOnStart,
|
|
style =
|
|
{
|
|
marginTop = 10,
|
|
marginLeft = 15
|
|
},
|
|
tooltip = "Opens the Sidekick character tool on Unity startup"
|
|
};
|
|
|
|
autoOpenToggle.RegisterValueChangedCallback(
|
|
evt =>
|
|
{
|
|
_openWindowOnStart = evt.newValue;
|
|
EditorPrefs.SetBool(_AUTO_OPEN_STATE, _openWindowOnStart);
|
|
}
|
|
);
|
|
|
|
view.Add(autoOpenToggle);
|
|
|
|
Toggle autoSaveToggle = new Toggle("Remember character")
|
|
{
|
|
value = _useAutoSaveAndLoad,
|
|
style =
|
|
{
|
|
marginTop = 10,
|
|
marginLeft = 15
|
|
},
|
|
tooltip = "Auto saves and loads character on run/stop and unity or tool open and close."
|
|
};
|
|
|
|
autoSaveToggle.RegisterValueChangedCallback(
|
|
evt =>
|
|
{
|
|
_useAutoSaveAndLoad = evt.newValue;
|
|
EditorPrefs.SetBool(_AUTOSAVE_STATE, _useAutoSaveAndLoad);
|
|
}
|
|
);
|
|
|
|
view.Add(autoSaveToggle);
|
|
|
|
// TODO: Change to showing presets when parts are missing
|
|
Toggle showMissingParts = new Toggle("Show missing parts warning")
|
|
{
|
|
value = _showMissingPartsPopup,
|
|
style =
|
|
{
|
|
marginTop = 10,
|
|
marginLeft = 15
|
|
},
|
|
tooltip = "Shows a popup for missing parts when selecting a preset."
|
|
};
|
|
|
|
showMissingParts.RegisterValueChangedCallback(
|
|
evt =>
|
|
{
|
|
_showMissingPartsPopup = evt.newValue;
|
|
EditorPrefs.SetBool(_AUTOSAVE_MISSING_PARTS, _showMissingPartsPopup);
|
|
}
|
|
);
|
|
|
|
view.Add(showMissingParts);
|
|
|
|
VisualElement row = new VisualElement
|
|
{
|
|
style =
|
|
{
|
|
flexDirection = new StyleEnum<FlexDirection>(FlexDirection.Row),
|
|
alignContent = new StyleEnum<Align>(Align.Center),
|
|
justifyContent = new StyleEnum<Justify>(Justify.SpaceAround),
|
|
marginTop = 30
|
|
}
|
|
};
|
|
|
|
Button documentationButton = new Button
|
|
{
|
|
text = "Documentation",
|
|
style =
|
|
{
|
|
width = Length.Percent(30)
|
|
},
|
|
tooltip = "Open documentation for Sidekick characters"
|
|
};
|
|
|
|
documentationButton.clickable.clicked += delegate
|
|
{
|
|
string documentationPath = Path.Combine(DatabaseManager.GetPackageRootAbsolutePath() ?? string.Empty, "Documentation", "SidekickCharacters_UserGuide.pdf");
|
|
documentationPath = Path.GetFullPath(documentationPath);
|
|
if (File.Exists(documentationPath))
|
|
{
|
|
Application.OpenURL("file:" + documentationPath);
|
|
}
|
|
};
|
|
|
|
Button storeButton = new Button
|
|
{
|
|
text = "Synty Store",
|
|
style =
|
|
{
|
|
width = Length.Percent(30)
|
|
},
|
|
tooltip = "www.syntystore.com"
|
|
};
|
|
|
|
storeButton.clickable.clicked += delegate
|
|
{
|
|
Application.OpenURL("https://syntystore.com");
|
|
};
|
|
|
|
Button tutorialButton = new Button
|
|
{
|
|
text = "Tutorials",
|
|
style =
|
|
{
|
|
width = Length.Percent(30)
|
|
},
|
|
tooltip = "Sidekick Characters - Quick start guide"
|
|
};
|
|
|
|
tutorialButton.clickable.clicked += delegate
|
|
{
|
|
// TODO: Change to direct link to tutorial playlist, when available
|
|
Application.OpenURL("https://www.youtube.com/@syntystudios");
|
|
};
|
|
|
|
row.Add(documentationButton);
|
|
row.Add(storeButton);
|
|
row.Add(tutorialButton);
|
|
view.Add(row);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reset the color rows for this color set back to the colors stored on the saved textures.
|
|
/// </summary>
|
|
private void ResetColorSet()
|
|
{
|
|
PopulateColorRowsFromTextures();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Shows the name field and creation button for creating a new color set.
|
|
/// </summary>
|
|
private void ShowCreateNewColorSet()
|
|
{
|
|
_newSetNameContainer.style.display = DisplayStyle.Flex;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new color set with the given name.
|
|
/// </summary>
|
|
/// <param name="setName">The name for the color set.</param>
|
|
private void CreateNewColorSet(string setName)
|
|
{
|
|
ResetCurrentColorSet(setName);
|
|
SaveColorSet();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the current color set to a new, in-memory set of rows independent of the database
|
|
/// </summary>
|
|
/// <param name="setName">Name of the new color set</param>
|
|
private void ResetCurrentColorSet(string setName = "Custom")
|
|
{
|
|
_currentColorSet.ID = -1;
|
|
_currentColorSet.Species = _currentSpecies;
|
|
_currentColorSet.Name = setName;
|
|
|
|
foreach (SidekickColorRow row in _allColorRows)
|
|
{
|
|
row.ID = -1;
|
|
row.ColorSet = _currentColorSet;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deletes a color set from the database. The color set will still be available in the app until the app is closed.
|
|
/// </summary>
|
|
private void DeleteColorSet()
|
|
{
|
|
List<SidekickColorRow> rowsToDelete = SidekickColorRow.GetAllBySet(_dbManager, _currentColorSet);
|
|
foreach (SidekickColorRow row in rowsToDelete)
|
|
{
|
|
row.Delete(_dbManager);
|
|
}
|
|
|
|
_currentColorSet.Delete(_dbManager);
|
|
UpdateVisibleColorSets();
|
|
ResetCurrentColorSet();
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// Saves the current color row to the database. If it is a new color row, it is inserted into the DB; otherwise it is updated in the DB.
|
|
/// </summary>
|
|
private void SaveColorSet()
|
|
{
|
|
string baseColorSetPath = DatabaseManager.GetPackageAssetPath("Resources", "Species") ?? _BASE_COLOR_SET_PATH;
|
|
string path = Path.Combine(baseColorSetPath, _currentSpecies.Name);
|
|
path = Path.Combine(path, _currentColorSet.Name.Replace(" ", "_"));
|
|
|
|
SaveTexturesToDisk(path);
|
|
|
|
_currentColorSet.Save(_dbManager);
|
|
foreach (SidekickColorRow row in _allColorRows)
|
|
{
|
|
row.Save(_dbManager);
|
|
}
|
|
|
|
UpdateVisibleColorSets(false);
|
|
|
|
// TODO : refresh the project inspector window so the new textures show up
|
|
}
|
|
|
|
/// <summary>
|
|
/// Saves texture files to disk at the given path.
|
|
/// </summary>
|
|
/// <param name="path">The path to save the textures to.</param>
|
|
private void SaveTexturesToDisk(string path, string additionalNaming = "")
|
|
{
|
|
if (!Directory.Exists(path))
|
|
{
|
|
Directory.CreateDirectory(path);
|
|
}
|
|
|
|
// if no parts are selected, don't try and save a non-existent material, instead create one
|
|
if (_currentMaterial == null)
|
|
{
|
|
_currentMaterial = (Material) _materialField.value;
|
|
}
|
|
|
|
string filename = _TEXTURE_PREFIX;
|
|
|
|
if (!string.IsNullOrEmpty(additionalNaming))
|
|
{
|
|
filename += additionalNaming;
|
|
}
|
|
|
|
string filePath = Path.Combine(path, filename + _TEXTURE_COLOR_NAME);
|
|
Texture2D texture = (Texture2D) _currentMaterial.GetTexture(_COLOR_MAP);
|
|
File.WriteAllBytes(filePath, texture.EncodeToPNG());
|
|
_currentColorSet.SourceColorPath = filePath;
|
|
// TODO: Hidden due to early access, enable when feature complete
|
|
// filePath = Path.Combine(path, filename + _TEXTURE_METALLIC_NAME);
|
|
// texture = (Texture2D) _currentMaterial.GetTexture(_METALLIC_MAP);
|
|
// File.WriteAllBytes(filePath, texture.EncodeToPNG());
|
|
// _currentColorSet.SourceMetallicPath = filePath;
|
|
// filePath = Path.Combine(path, filename + _TEXTURE_SMOOTHNESS_NAME);
|
|
// texture = (Texture2D) _currentMaterial.GetTexture(_SMOOTHNESS_MAP);
|
|
// File.WriteAllBytes(filePath, texture.EncodeToPNG());
|
|
// _currentColorSet.SourceSmoothnessPath = filePath;
|
|
// filePath = Path.Combine(path, filename + _TEXTURE_REFLECTION_NAME);
|
|
// texture = (Texture2D) _currentMaterial.GetTexture(_REFLECTION_MAP);
|
|
// File.WriteAllBytes(filePath, texture.EncodeToPNG());
|
|
// _currentColorSet.SourceReflectionPath = filePath;
|
|
// filePath = Path.Combine(path, filename + _TEXTURE_EMISSION_NAME);
|
|
// texture = (Texture2D) _currentMaterial.GetTexture(_EMISSION_MAP);
|
|
// File.WriteAllBytes(filePath, texture.EncodeToPNG());
|
|
// _currentColorSet.SourceEmissionPath = filePath;
|
|
// filePath = Path.Combine(path, filename + _TEXTURE_OPACITY_NAME);
|
|
// texture = (Texture2D) _currentMaterial.GetTexture(_OPACITY_MAP);
|
|
// File.WriteAllBytes(filePath, texture.EncodeToPNG());
|
|
// _currentColorSet.SourceOpacityPath = filePath;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the color sets that are selectable in the dropdown on the colors tab
|
|
/// </summary>
|
|
/// <param name="setDropdownToCustom">Whether to set the color sets dropdown value to 'Custom'</param>
|
|
private void UpdateVisibleColorSets(bool setDropdownToCustom = true)
|
|
{
|
|
List<SidekickColorSet> sets = SidekickColorSet.GetAllBySpecies(_dbManager, _currentSpecies);
|
|
if (sets.Count == 0)
|
|
{
|
|
sets.Add(SidekickColorSet.GetDefault(_dbManager));
|
|
}
|
|
|
|
List<string> setNames = sets.Select(set => set.Name).ToList();
|
|
_colorSetsDropdown.choices = setNames;
|
|
_visibleColorSets = sets;
|
|
|
|
if (setDropdownToCustom)
|
|
{
|
|
_colorSetsDropdown.value = "Custom";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates all the color rows currently visible in the UI.
|
|
/// </summary>
|
|
private void UpdateAllVisibleColors()
|
|
{
|
|
foreach (SidekickColorRow row in _visibleColorRows)
|
|
{
|
|
UpdateAllColors(row);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates all the color types for a given color row.
|
|
/// </summary>
|
|
/// <param name="colorRow">The color row to update.</param>
|
|
private void UpdateAllColors(SidekickColorRow colorRow)
|
|
{
|
|
foreach (ColorType colorType in Enum.GetValues(typeof(ColorType)))
|
|
{
|
|
_sidekickRuntime.UpdateColor(colorType, colorRow);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Populates the part color rows based on the filter being used.
|
|
/// </summary>
|
|
private void PopulatePartColorRows()
|
|
{
|
|
List<SidekickColorProperty> propertiesToShow = new List<SidekickColorProperty>();
|
|
|
|
switch (_currentPartType)
|
|
{
|
|
case ColorPartType.Species:
|
|
List<SidekickColorProperty> speciesProperties = SidekickColorProperty.GetAllByGroup(_dbManager, ColorGroup.Species);
|
|
foreach (SidekickColorProperty property in speciesProperties)
|
|
{
|
|
Vector2 uv = new Vector2(property.U, property.V);
|
|
if ((_currentUVList.Contains(uv) || _showAllColourProperties == true) && !propertiesToShow.Contains(property))
|
|
{
|
|
propertiesToShow.Add(property);
|
|
}
|
|
}
|
|
|
|
break;
|
|
case ColorPartType.Outfit:
|
|
List<SidekickColorProperty> outfitProperties = SidekickColorProperty.GetAllByGroup(_dbManager, ColorGroup.Outfits);
|
|
foreach (SidekickColorProperty property in outfitProperties)
|
|
{
|
|
Vector2 uv = new Vector2(property.U, property.V);
|
|
if ((_currentUVList.Contains(uv) || _showAllColourProperties == true) && !propertiesToShow.Contains(property))
|
|
{
|
|
propertiesToShow.Add(property);
|
|
}
|
|
}
|
|
|
|
break;
|
|
case ColorPartType.Attachments:
|
|
List<SidekickColorProperty> attachmentProperties = SidekickColorProperty.GetAllByGroup(_dbManager, ColorGroup.Attachments);
|
|
foreach (SidekickColorProperty property in attachmentProperties)
|
|
{
|
|
Vector2 uv = new Vector2(property.U, property.V);
|
|
if ((_currentUVList.Contains(uv) || _showAllColourProperties == true) && !propertiesToShow.Contains(property))
|
|
{
|
|
propertiesToShow.Add(property);
|
|
}
|
|
}
|
|
|
|
break;
|
|
case ColorPartType.Materials:
|
|
List<SidekickColorProperty> materialProperties = SidekickColorProperty.GetAllByGroup(_dbManager, ColorGroup.Materials);
|
|
foreach (SidekickColorProperty property in materialProperties)
|
|
{
|
|
Vector2 uv = new Vector2(property.U, property.V);
|
|
if ((_currentUVList.Contains(uv) || _showAllColourProperties == true) && !propertiesToShow.Contains(property))
|
|
{
|
|
propertiesToShow.Add(property);
|
|
}
|
|
}
|
|
|
|
break;
|
|
case ColorPartType.Elements:
|
|
List<SidekickColorProperty> elementProperties = SidekickColorProperty.GetAllByGroup(_dbManager, ColorGroup.Elements);
|
|
foreach (SidekickColorProperty property in elementProperties)
|
|
{
|
|
Vector2 uv = new Vector2(property.U, property.V);
|
|
if ((_currentUVList.Contains(uv) || _showAllColourProperties == true) && !propertiesToShow.Contains(property))
|
|
{
|
|
propertiesToShow.Add(property);
|
|
}
|
|
}
|
|
|
|
break;
|
|
case ColorPartType.CharacterHead:
|
|
List<SidekickColorProperty> headProperties = new List<SidekickColorProperty>();
|
|
foreach (ColorPartType type in ColorPartType.CharacterHead.GetPartTypes())
|
|
{
|
|
headProperties.AddRange(SidekickColorProperty.GetByUVs(_dbManager, _currentUVDictionary[type]));
|
|
}
|
|
|
|
foreach (SidekickColorProperty property in headProperties)
|
|
{
|
|
if (!propertiesToShow.Contains(property))
|
|
{
|
|
propertiesToShow.Add(property);
|
|
}
|
|
}
|
|
|
|
break;
|
|
case ColorPartType.CharacterUpperBody:
|
|
List<SidekickColorProperty> upperProperties = new List<SidekickColorProperty>();
|
|
foreach (ColorPartType type in ColorPartType.CharacterUpperBody.GetPartTypes())
|
|
{
|
|
upperProperties.AddRange(SidekickColorProperty.GetByUVs(_dbManager, _currentUVDictionary[type]));
|
|
}
|
|
|
|
foreach (SidekickColorProperty property in upperProperties)
|
|
{
|
|
if (!propertiesToShow.Contains(property))
|
|
{
|
|
propertiesToShow.Add(property);
|
|
}
|
|
}
|
|
|
|
break;
|
|
case ColorPartType.CharacterLowerBody:
|
|
List<SidekickColorProperty> lowerProperties = new List<SidekickColorProperty>();
|
|
foreach (ColorPartType type in ColorPartType.CharacterLowerBody.GetPartTypes())
|
|
{
|
|
lowerProperties.AddRange(SidekickColorProperty.GetByUVs(_dbManager, _currentUVDictionary[type]));
|
|
}
|
|
|
|
foreach (SidekickColorProperty property in lowerProperties)
|
|
{
|
|
if (!propertiesToShow.Contains(property))
|
|
{
|
|
propertiesToShow.Add(property);
|
|
}
|
|
}
|
|
|
|
break;
|
|
case ColorPartType.Head:
|
|
case ColorPartType.Hair:
|
|
case ColorPartType.EyebrowLeft:
|
|
case ColorPartType.EyebrowRight:
|
|
case ColorPartType.EyeLeft:
|
|
case ColorPartType.EyeRight:
|
|
case ColorPartType.EarLeft:
|
|
case ColorPartType.EarRight:
|
|
case ColorPartType.FacialHair:
|
|
case ColorPartType.Torso:
|
|
case ColorPartType.ArmUpperLeft:
|
|
case ColorPartType.ArmUpperRight:
|
|
case ColorPartType.ArmLowerLeft:
|
|
case ColorPartType.ArmLowerRight:
|
|
case ColorPartType.HandLeft:
|
|
case ColorPartType.HandRight:
|
|
case ColorPartType.Hips:
|
|
case ColorPartType.LegLeft:
|
|
case ColorPartType.LegRight:
|
|
case ColorPartType.FootLeft:
|
|
case ColorPartType.FootRight:
|
|
case ColorPartType.AttachmentHead:
|
|
case ColorPartType.AttachmentFace:
|
|
case ColorPartType.AttachmentBack:
|
|
case ColorPartType.AttachmentHipsFront:
|
|
case ColorPartType.AttachmentHipsBack:
|
|
case ColorPartType.AttachmentHipsLeft:
|
|
case ColorPartType.AttachmentHipsRight:
|
|
case ColorPartType.AttachmentShoulderLeft:
|
|
case ColorPartType.AttachmentShoulderRight:
|
|
case ColorPartType.AttachmentElbowLeft:
|
|
case ColorPartType.AttachmentElbowRight:
|
|
case ColorPartType.AttachmentKneeLeft:
|
|
case ColorPartType.AttachmentKneeRight:
|
|
case ColorPartType.Nose:
|
|
case ColorPartType.Teeth:
|
|
case ColorPartType.Tongue:
|
|
case ColorPartType.Wrap:/*
|
|
case ColorPartType.AttachmentHandLeft:
|
|
case ColorPartType.AttachmentHandRight: */
|
|
propertiesToShow = SidekickColorProperty.GetByUVs(_dbManager, _currentUVDictionary[_currentPartType]);
|
|
break;
|
|
case ColorPartType.AllParts:
|
|
default:
|
|
propertiesToShow = _showAllColourProperties || _currentUVList == null
|
|
? SidekickColorProperty.GetAll(_dbManager)
|
|
: SidekickColorProperty.GetByUVs(_dbManager, _currentUVList);
|
|
break;
|
|
}
|
|
|
|
_visibleColorRows.Clear();
|
|
|
|
// when filtering the view to a specific UV dictionary, we need to reset the property order
|
|
propertiesToShow.Sort((a, b) => a.ID.CompareTo(b.ID));
|
|
|
|
foreach (SidekickColorProperty property in propertiesToShow)
|
|
{
|
|
foreach (SidekickColorRow row in _allColorRows.Where(row => row.ColorProperty.ID == property.ID))
|
|
{
|
|
_visibleColorRows.Add(row);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Refreshes the visible color rows in the UI.
|
|
/// </summary>
|
|
private void RefreshVisibleColorRows()
|
|
{
|
|
_colorSelectionRowView.Clear();
|
|
|
|
foreach (ColorGroup group in Enum.GetValues(typeof(ColorGroup)))
|
|
{
|
|
List<SidekickColorProperty> properties = _visibleColorRows
|
|
.Select(row => row.ColorProperty)
|
|
.Where(prop => prop.Group == group)
|
|
.ToList();
|
|
|
|
string tooltipText = "";
|
|
|
|
switch (group)
|
|
{
|
|
case ColorGroup.Species:
|
|
tooltipText = "Species colors make up the character as if it has no outfit on. (for example - skin, teeth, tongue, fingernails etc)";
|
|
break;
|
|
case ColorGroup.Outfits:
|
|
tooltipText = "Outfit colors make up the clothing on the character. (for example - Torso outfit, arm outfit, hand outfit etc)";
|
|
break;
|
|
case ColorGroup.Attachments:
|
|
tooltipText = "Attachment colors make up the additional parts attached to a character. (for example - a backpack, shoulder pads, elbow pads, knee pads etc)";
|
|
break;
|
|
case ColorGroup.Materials:
|
|
tooltipText = "material colors make up a collection of shared standard materials (for example - wood, metal, leather, paper, bone etc)";
|
|
break;
|
|
}
|
|
|
|
if (properties.Count > 0)
|
|
{
|
|
Label groupLabel = new Label(group.ToString())
|
|
{
|
|
style =
|
|
{
|
|
unityFontStyleAndWeight = new StyleEnum<FontStyle>(FontStyle.Bold),
|
|
marginBottom = 4,
|
|
marginTop = 6
|
|
},
|
|
tooltip = tooltipText
|
|
};
|
|
_colorSelectionRowView.Add(groupLabel);
|
|
|
|
VisualElement headerContainer = new VisualElement()
|
|
{
|
|
style =
|
|
{
|
|
flexDirection = new StyleEnum<FlexDirection>(FlexDirection.Row),
|
|
fontSize = 10,
|
|
unityFontStyleAndWeight = new StyleEnum<FontStyle>(FontStyle.Bold)
|
|
}
|
|
};
|
|
|
|
Label colorHeader = new Label("Color")
|
|
{
|
|
style =
|
|
{
|
|
width = 103,
|
|
marginLeft = 155
|
|
}
|
|
};
|
|
Label metallicHeader = new Label("Metallic")
|
|
{
|
|
style =
|
|
{
|
|
width = 66
|
|
}
|
|
};
|
|
Label smoothnessHeader = new Label("Smoothness")
|
|
{
|
|
style =
|
|
{
|
|
width = 66
|
|
}
|
|
};
|
|
Label reflectionHeader = new Label("Reflection")
|
|
{
|
|
style =
|
|
{
|
|
width = 66
|
|
}
|
|
};
|
|
Label emissionHeader = new Label("Emission")
|
|
{
|
|
style =
|
|
{
|
|
width = 66
|
|
}
|
|
};
|
|
Label opacityHeader = new Label("Opacity")
|
|
{
|
|
style =
|
|
{
|
|
width = 66
|
|
}
|
|
};
|
|
|
|
headerContainer.Add(colorHeader);
|
|
// headerContainer.Add(metallicHeader);
|
|
// headerContainer.Add(smoothnessHeader);
|
|
// headerContainer.Add(reflectionHeader);
|
|
// headerContainer.Add(emissionHeader);
|
|
// headerContainer.Add(opacityHeader);
|
|
|
|
_colorSelectionRowView.Add(headerContainer);
|
|
|
|
foreach (SidekickColorProperty property in properties)
|
|
{
|
|
foreach (SidekickColorRow row in _visibleColorRows.Where(row => row.ColorProperty.ID == property.ID))
|
|
{
|
|
CreateColorRow(_colorSelectionRowView, row);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Populates the color rows from texture files on the disk.
|
|
/// </summary>
|
|
private void PopulateColorRowsFromTextures()
|
|
{
|
|
TextureImporter textureImporter = null;
|
|
|
|
Texture2D mainColor = AssetDatabase.LoadAssetAtPath<Texture2D>(_currentColorSet.SourceColorPath);
|
|
if (mainColor != null)
|
|
{
|
|
mainColor.filterMode = FilterMode.Point;
|
|
if (!mainColor.isReadable)
|
|
{
|
|
textureImporter = (TextureImporter) AssetImporter.GetAtPath(_currentColorSet.SourceColorPath);
|
|
textureImporter.isReadable = true;
|
|
textureImporter.SaveAndReimport();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Material defaultMaterial = (Material) _materialField.value;
|
|
mainColor = (Texture2D) defaultMaterial.mainTexture;
|
|
}
|
|
|
|
Texture2D metallic = AssetDatabase.LoadAssetAtPath<Texture2D>(_currentColorSet.SourceMetallicPath);
|
|
if (metallic != null)
|
|
{
|
|
metallic.filterMode = FilterMode.Point;
|
|
if (!metallic.isReadable)
|
|
{
|
|
textureImporter = (TextureImporter) AssetImporter.GetAtPath(_currentColorSet.SourceMetallicPath);
|
|
textureImporter.isReadable = true;
|
|
textureImporter.SaveAndReimport();
|
|
}
|
|
}
|
|
|
|
Texture2D smoothness = AssetDatabase.LoadAssetAtPath<Texture2D>(_currentColorSet.SourceSmoothnessPath);
|
|
if (smoothness != null)
|
|
{
|
|
smoothness.filterMode = FilterMode.Point;
|
|
if (!smoothness.isReadable)
|
|
{
|
|
textureImporter = (TextureImporter) AssetImporter.GetAtPath(_currentColorSet.SourceSmoothnessPath);
|
|
textureImporter.isReadable = true;
|
|
textureImporter.SaveAndReimport();
|
|
}
|
|
}
|
|
|
|
Texture2D reflection = AssetDatabase.LoadAssetAtPath<Texture2D>(_currentColorSet.SourceReflectionPath);
|
|
if (reflection != null)
|
|
{
|
|
reflection.filterMode = FilterMode.Point;
|
|
if (!reflection.isReadable)
|
|
{
|
|
textureImporter = (TextureImporter) AssetImporter.GetAtPath(_currentColorSet.SourceReflectionPath);
|
|
textureImporter.isReadable = true;
|
|
textureImporter.SaveAndReimport();
|
|
}
|
|
}
|
|
|
|
Texture2D emission = AssetDatabase.LoadAssetAtPath<Texture2D>(_currentColorSet.SourceEmissionPath);
|
|
if (emission != null)
|
|
{
|
|
emission.filterMode = FilterMode.Point;
|
|
if (!emission.isReadable)
|
|
{
|
|
textureImporter = (TextureImporter) AssetImporter.GetAtPath(_currentColorSet.SourceEmissionPath);
|
|
textureImporter.isReadable = true;
|
|
textureImporter.SaveAndReimport();
|
|
}
|
|
}
|
|
|
|
Texture2D opacity = AssetDatabase.LoadAssetAtPath<Texture2D>(_currentColorSet.SourceOpacityPath);
|
|
if (opacity != null)
|
|
{
|
|
opacity.filterMode = FilterMode.Point;
|
|
if (!opacity.isReadable)
|
|
{
|
|
textureImporter = (TextureImporter) AssetImporter.GetAtPath(_currentColorSet.SourceOpacityPath);
|
|
textureImporter.isReadable = true;
|
|
textureImporter.SaveAndReimport();
|
|
}
|
|
}
|
|
|
|
List<SidekickColorRow> newColorRows = new List<SidekickColorRow>();
|
|
|
|
List<SidekickColorRow> currentSetColors = SidekickColorRow.GetAllBySet(_dbManager, _currentColorSet);
|
|
|
|
// TODO : if textures don't exist BUT color rows exist in DB, ask user if they want to re-save the textures from the DB values, loop back and reimport
|
|
// TODO : if textures don't exist AND color rows don't exist in DB, delete the colorset entry in DB/dropdown, advance to next on list and reload
|
|
|
|
foreach (SidekickColorProperty property in SidekickColorProperty.GetAll(_dbManager))
|
|
{
|
|
SidekickColorRow existingRow = currentSetColors.FirstOrDefault(row => row.ColorProperty.ID == property.ID);
|
|
SidekickColorRow newRow = new SidekickColorRow
|
|
{
|
|
ID = existingRow?.ID ?? -1,
|
|
ColorSet = _currentColorSet,
|
|
ColorProperty = property,
|
|
// TODO remove null checks when we know we have textures
|
|
NiceColor = mainColor?.GetPixel(property.U * 2, property.V * 2) ?? existingRow?.NiceColor ?? Color.red,
|
|
// NiceMetallic = metallic?.GetPixel(property.U * 2, property.V * 2) ?? existingRow?.NiceMetallic ?? Color.red,
|
|
// NiceSmoothness = smoothness?.GetPixel(property.U * 2, property.V * 2) ?? existingRow?.NiceSmoothness ?? Color.red,
|
|
// NiceReflection = reflection?.GetPixel(property.U * 2, property.V * 2) ?? existingRow?.NiceReflection ?? Color.red,
|
|
// NiceEmission = emission?.GetPixel(property.U * 2, property.V * 2) ?? existingRow?.NiceEmission ?? Color.red,
|
|
// NiceOpacity = opacity?.GetPixel(property.U * 2, property.V * 2) ?? existingRow?.NiceOpacity ?? Color.red
|
|
};
|
|
|
|
newRow.Save(_dbManager);
|
|
newColorRows.Add(newRow);
|
|
}
|
|
|
|
_allColorRows = newColorRows;
|
|
PopulatePartColorRows();
|
|
RefreshVisibleColorRows();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a color row to the given view.
|
|
/// </summary>
|
|
/// <param name="view">The view to add the color row to.</param>
|
|
/// <param name="colorRow">The color row to populate this UI element with.</param>
|
|
private void CreateColorRow(VisualElement view, SidekickColorRow colorRow)
|
|
{
|
|
VisualElement row = new VisualElement();
|
|
row.AddToClassList("colorSelectionRow");
|
|
|
|
Label rowLabel = new Label(colorRow.ColorProperty.Name);
|
|
rowLabel.AddToClassList("colorSelectionRowLabel");
|
|
row.Add(rowLabel);
|
|
|
|
VisualElement rowContent = new VisualElement();
|
|
rowContent.AddToClassList("colorSelectionRowContent");
|
|
|
|
row.Add(rowContent);
|
|
|
|
// TODO: uncomment when locking is required.
|
|
// Button btnLock = new Button();
|
|
//
|
|
// Image lockImage = new Image
|
|
// {
|
|
// image = colorRow.IsLocked ? EditorGUIUtility.IconContent("Locked").image : EditorGUIUtility.IconContent("Unlocked").image,
|
|
// scaleMode = ScaleMode.ScaleToFit
|
|
// };
|
|
//
|
|
// btnLock.Add(lockImage);
|
|
// colorRow.ButtonImage = lockImage;
|
|
// rowContent.Add(btnLock);
|
|
// btnLock.clickable.clicked += () =>
|
|
// {
|
|
// colorRow.IsLocked = !colorRow.IsLocked;
|
|
// lockImage.image = colorRow.IsLocked
|
|
// ? EditorGUIUtility.IconContent("Locked").image
|
|
// : EditorGUIUtility.IconContent("Unlocked").image;
|
|
// };
|
|
|
|
ColorField colorField = new ColorField
|
|
{
|
|
value = colorRow.NiceColor,
|
|
tooltip = colorRow.ColorProperty.Name + " Color",
|
|
style =
|
|
{
|
|
// TODO: shrink to 50 once all colors options are re-enabled
|
|
width = 100
|
|
}
|
|
};
|
|
rowContent.Add(colorField);
|
|
colorField.RegisterValueChangedCallback(
|
|
evt =>
|
|
{
|
|
colorRow.NiceColor = evt.newValue;
|
|
_sidekickRuntime.UpdateColor(ColorType.MainColor, colorRow);
|
|
}
|
|
);
|
|
|
|
// TODO: Uncomment once all colors options are re-enabled
|
|
// ColorField metallicField = new ColorField
|
|
// {
|
|
// value = colorRow.NiceMetallic,
|
|
// tooltip = colorRow.ColorProperty.Name + " Metallic",
|
|
// style =
|
|
// {
|
|
// width = 60
|
|
// }
|
|
// };
|
|
// rowContent.Add(metallicField);
|
|
// metallicField.RegisterValueChangedCallback(
|
|
// evt =>
|
|
// {
|
|
// colorRow.NiceMetallic = evt.newValue;
|
|
// _sidekickRuntime.UpdateColor(ColorType.Metallic, colorRow);
|
|
// }
|
|
// );
|
|
//
|
|
// ColorField smoothnessField = new ColorField
|
|
// {
|
|
// value = colorRow.NiceSmoothness,
|
|
// tooltip = colorRow.ColorProperty.Name + " Smoothness",
|
|
// style =
|
|
// {
|
|
// width = 60
|
|
// }
|
|
// };
|
|
// rowContent.Add(smoothnessField);
|
|
// smoothnessField.RegisterValueChangedCallback(
|
|
// evt =>
|
|
// {
|
|
// colorRow.NiceSmoothness = evt.newValue;
|
|
// _sidekickRuntime.UpdateColor(ColorType.Smoothness, colorRow);
|
|
// }
|
|
// );
|
|
//
|
|
// ColorField reflectionField = new ColorField
|
|
// {
|
|
// value = colorRow.NiceReflection,
|
|
// tooltip = colorRow.ColorProperty.Name + " Reflection",
|
|
// style =
|
|
// {
|
|
// width = 60
|
|
// }
|
|
// };
|
|
// rowContent.Add(reflectionField);
|
|
// reflectionField.RegisterValueChangedCallback(
|
|
// evt =>
|
|
// {
|
|
// colorRow.NiceReflection = evt.newValue;
|
|
// _sidekickRuntime.UpdateColor(ColorType.Reflection, colorRow);
|
|
// }
|
|
// );
|
|
//
|
|
// ColorField emissionField = new ColorField
|
|
// {
|
|
// value = colorRow.NiceEmission,
|
|
// tooltip = colorRow.ColorProperty.Name + " Emission",
|
|
// style =
|
|
// {
|
|
// width = 60
|
|
// }
|
|
// };
|
|
// rowContent.Add(emissionField);
|
|
// emissionField.RegisterValueChangedCallback(
|
|
// evt =>
|
|
// {
|
|
// colorRow.NiceEmission = evt.newValue;
|
|
// _sidekickRuntime.UpdateColor(ColorType.Emission, colorRow);
|
|
// }
|
|
// );
|
|
//
|
|
// ColorField opacityField = new ColorField
|
|
// {
|
|
// value = colorRow.NiceOpacity,
|
|
// tooltip = colorRow.ColorProperty.Name + " Opacity",
|
|
// style =
|
|
// {
|
|
// width = 60
|
|
// }
|
|
// };
|
|
// rowContent.Add(opacityField);
|
|
// opacityField.RegisterValueChangedCallback(
|
|
// evt =>
|
|
// {
|
|
// colorRow.NiceOpacity = evt.newValue;
|
|
// _sidekickRuntime.UpdateColor(ColorType.Opacity, colorRow);
|
|
// }
|
|
// );
|
|
|
|
// Button randomButton = new Button
|
|
// {
|
|
// text = "R",
|
|
// style =
|
|
// {
|
|
// right = 0
|
|
// }
|
|
// };
|
|
// rowContent.Add(randomButton);
|
|
|
|
view.Add(row);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Switches the currently visible tab to the given tab.
|
|
/// </summary>
|
|
/// <param name="newTab">The tab to switch to.</param>
|
|
private void SwitchToTab(TabView newTab)
|
|
{
|
|
if (_currentTab == newTab)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_currentTab = newTab;
|
|
|
|
_bodyPresetTab.value = _currentTab == TabView.Preset;
|
|
_bodyPartsTab.value = _currentTab == TabView.Parts;
|
|
_bodyShapeTab.value = _currentTab == TabView.Body;
|
|
_colorSelectionTab.value = _currentTab == TabView.Colors;
|
|
// _decalSelectionTab.value = _currentTab == TabView.Decals;
|
|
_optionTab.value = _currentTab == TabView.Options;
|
|
|
|
_presetView.style.display = _bodyPresetTab.value ? DisplayStyle.Flex : DisplayStyle.None;
|
|
_partView.style.display = _bodyPartsTab.value ? DisplayStyle.Flex : DisplayStyle.None;
|
|
_bodyShapeView.style.display = _bodyShapeTab.value ? DisplayStyle.Flex : DisplayStyle.None;
|
|
_colorSelectionView.style.display = _colorSelectionTab.value ? DisplayStyle.Flex : DisplayStyle.None;
|
|
//_decalSelectionView.style.display = _decalSelectionTab.value ? DisplayStyle.Flex : DisplayStyle.None;
|
|
_optionSelectionView.style.display = _optionTab.value ? DisplayStyle.Flex : DisplayStyle.None;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Populate the preset tab content.
|
|
/// </summary>
|
|
private void PopulatePresetUI()
|
|
{
|
|
_presetView.Clear();
|
|
|
|
Dictionary<string, PopupField<string>> dropdowns = new Dictionary<string, PopupField<string>>();
|
|
|
|
Foldout speciesFoldout = new Foldout
|
|
{
|
|
text = "Select - Species",
|
|
style =
|
|
{
|
|
unityFontStyleAndWeight = new StyleEnum<FontStyle>(FontStyle.Bold)
|
|
},
|
|
};
|
|
|
|
List<string> speciesNames = _allSpecies.Select(species => species.Name).ToList();
|
|
|
|
_speciesPresetField = new DropdownField
|
|
{
|
|
label = "Species",
|
|
style =
|
|
{
|
|
unityFontStyleAndWeight = new StyleEnum<FontStyle>(FontStyle.Normal)
|
|
},
|
|
tooltip = "Select the species of your character"
|
|
};
|
|
_speciesPresetField.choices = speciesNames;
|
|
_speciesPresetField.RegisterValueChangedCallback(
|
|
evt =>
|
|
{
|
|
_speciesField.value = _speciesPresetField.value;
|
|
ProcessSpeciesChange(evt.newValue);
|
|
}
|
|
);
|
|
|
|
_speciesPresetField.index = _currentSpecies != null && speciesNames.Count > 0 ? _speciesPresetField.choices.IndexOf(_currentSpecies.Name) : 0;
|
|
speciesFoldout.Add(_speciesPresetField);
|
|
_presetView.Add(speciesFoldout);
|
|
|
|
List<SidekickPresetFilter> allFilters = SidekickPresetFilter.GetAll(_dbManager);
|
|
|
|
allFilters.Sort(
|
|
(filterA, filterB) => String.CompareOrdinal(filterA.Term, filterB.Term)
|
|
);
|
|
|
|
if (allFilters.Count > 0)
|
|
{
|
|
Foldout filterFoldout = new Foldout()
|
|
{
|
|
text = "Select - Preset Part Filter",
|
|
style =
|
|
{
|
|
unityFontStyleAndWeight = new StyleEnum<FontStyle>(FontStyle.Bold)
|
|
}
|
|
};
|
|
|
|
VisualElement filterContent = new VisualElement
|
|
{
|
|
style =
|
|
{
|
|
flexDirection = new StyleEnum<FlexDirection>(FlexDirection.Row),
|
|
flexWrap = new StyleEnum<Wrap>(Wrap.Wrap)
|
|
}
|
|
};
|
|
|
|
Color borderColor = new Color(0.17f, 0.17f, 0.17f);
|
|
Color backgroundColor = new Color(0.35f, 0.35f, 0.35f);
|
|
List<Toggle> allFilterToggles = new List<Toggle>();
|
|
|
|
foreach (SidekickPresetFilter filter in allFilters)
|
|
{
|
|
Toggle outfitToggle = new Toggle(filter.Term)
|
|
{
|
|
value = !_partPresetFilterToggleMap.TryGetValue(filter, out bool toggleValue) || toggleValue,
|
|
style =
|
|
{
|
|
width = 160,
|
|
borderBottomWidth = 1,
|
|
borderBottomColor = borderColor,
|
|
paddingBottom = 2,
|
|
borderLeftWidth = 1,
|
|
borderLeftColor = borderColor,
|
|
paddingLeft = 2,
|
|
borderRightWidth = 1,
|
|
borderRightColor = borderColor,
|
|
paddingRight = 2,
|
|
borderTopWidth = 1,
|
|
borderTopColor = borderColor,
|
|
paddingTop = 2,
|
|
borderBottomLeftRadius = 3,
|
|
borderBottomRightRadius = 3,
|
|
borderTopLeftRadius = 3,
|
|
borderTopRightRadius = 3,
|
|
backgroundColor = backgroundColor,
|
|
textOverflow = new StyleEnum<TextOverflow>(TextOverflow.Ellipsis)
|
|
}
|
|
};
|
|
|
|
allFilterToggles.Add(outfitToggle);
|
|
|
|
if (outfitToggle.value)
|
|
{
|
|
_partPresetFilterToggleMap[filter] = outfitToggle.value;
|
|
}
|
|
|
|
outfitToggle.RegisterValueChangedCallback(
|
|
evt =>
|
|
{
|
|
_partPresetFilterToggleMap[filter] = evt.newValue;
|
|
PopulatePresetPartDropdowns(dropdowns);
|
|
}
|
|
);
|
|
|
|
filterContent.Add(outfitToggle);
|
|
}
|
|
|
|
VisualElement buttonRow = new VisualElement()
|
|
{
|
|
style =
|
|
{
|
|
flexDirection = new StyleEnum<FlexDirection>(FlexDirection.Row)
|
|
}
|
|
};
|
|
|
|
Button selectAll = new Button(
|
|
delegate
|
|
{
|
|
foreach (Toggle toggle in allFilterToggles)
|
|
{
|
|
toggle.value = true;
|
|
}
|
|
}
|
|
)
|
|
{
|
|
text = "Select All"
|
|
};
|
|
|
|
Button selectNone = new Button(
|
|
delegate
|
|
{
|
|
foreach (Toggle toggle in allFilterToggles)
|
|
{
|
|
toggle.value = false;
|
|
}
|
|
}
|
|
)
|
|
{
|
|
text = "Select None"
|
|
};
|
|
|
|
buttonRow.Add(selectAll);
|
|
buttonRow.Add(selectNone);
|
|
|
|
filterFoldout.Add(buttonRow);
|
|
filterFoldout.Add(filterContent);
|
|
|
|
_presetView.Add(filterFoldout);
|
|
}
|
|
|
|
_availablePresets = SidekickPartPreset.GetAll(_dbManager);
|
|
|
|
Foldout generateFoldout = new Foldout
|
|
{
|
|
text = "Randomize Character",
|
|
style =
|
|
{
|
|
unityFontStyleAndWeight = new StyleEnum<FontStyle>(FontStyle.Bold)
|
|
},
|
|
//tooltip = "Create a character based on the selected species"
|
|
};
|
|
|
|
Button generateButton = new Button()
|
|
{
|
|
style =
|
|
{
|
|
minHeight = 50,
|
|
marginRight = 18,
|
|
flexDirection = new StyleEnum<FlexDirection>(FlexDirection.Row),
|
|
alignContent = new StyleEnum<Align>(Align.Center),
|
|
alignItems = new StyleEnum<Align>(Align.Center),
|
|
unityTextAlign = new StyleEnum<TextAnchor>(TextAnchor.MiddleCenter),
|
|
justifyContent = new StyleEnum<Justify>(Justify.Center)
|
|
},
|
|
tooltip = "Generate a character at the push of a button"
|
|
};
|
|
|
|
Texture2D randomImage = Resources.Load<Texture2D>("UI/T_Random");
|
|
generateButton.Add(
|
|
new Image
|
|
{
|
|
image = randomImage,
|
|
scaleMode = ScaleMode.ScaleToFit,
|
|
style =
|
|
{
|
|
paddingTop = new StyleLength(1),
|
|
paddingBottom = new StyleLength(1),
|
|
paddingRight = 5,
|
|
alignSelf = new StyleEnum<Align>(Align.Center)
|
|
}
|
|
}
|
|
);
|
|
generateButton.Add(
|
|
new Label
|
|
{
|
|
text = "Randomize Character",
|
|
style =
|
|
{
|
|
alignSelf = new StyleEnum<Align>(Align.Center)
|
|
}
|
|
}
|
|
);
|
|
|
|
generateFoldout.Add(generateButton);
|
|
_presetView.Add(generateFoldout);
|
|
|
|
Foldout presetsFoldout = new Foldout()
|
|
{
|
|
text = "Presets",
|
|
style =
|
|
{
|
|
unityFontStyleAndWeight = new StyleEnum<FontStyle>(FontStyle.Bold)
|
|
},
|
|
//tooltip = "Select from a number of collections of parts, body types and colors"
|
|
};
|
|
|
|
Label partTitle = new Label("Parts")
|
|
{
|
|
style =
|
|
{
|
|
unityFontStyleAndWeight = new StyleEnum<FontStyle>(FontStyle.Bold)
|
|
},
|
|
tooltip = "Collections of character parts ie. Head attachment, Torso, Nose that make up the character"
|
|
};
|
|
|
|
string tooltipText = "";
|
|
|
|
presetsFoldout.Add(partTitle);
|
|
_presetView.Add(presetsFoldout);
|
|
|
|
_presetPartContainer = new VisualElement();
|
|
presetsFoldout.Add(_presetPartContainer);
|
|
|
|
PopulatePresetPartDropdowns(dropdowns);
|
|
|
|
Label bodyTitle = new Label("Body")
|
|
{
|
|
style =
|
|
{
|
|
unityFontStyleAndWeight = new StyleEnum<FontStyle>(FontStyle.Bold)
|
|
},
|
|
tooltip = "Preset bodies in a number of types, sizes and musculature"
|
|
};
|
|
|
|
presetsFoldout.Add(bodyTitle);
|
|
_currentBodyPresetDictionary = new Dictionary<string, SidekickBodyShapePreset>();
|
|
List<SidekickBodyShapePreset> bodyShapes = SidekickBodyShapePreset.GetAll(_dbManager);
|
|
List<string> bodyShapeNames = bodyShapes.Select(b => b.Name).ToList();
|
|
for (int i = 0; i < bodyShapeNames.Count; i++)
|
|
{
|
|
_currentBodyPresetDictionary.Add(bodyShapeNames[i], bodyShapes[i]);
|
|
}
|
|
|
|
string bodyTypeLabel = "Body Type";
|
|
|
|
string bodyShapeDefaultValue = "Androgynous Medium";
|
|
|
|
if (bodyShapeNames.Count > 0)
|
|
{
|
|
bodyShapeDefaultValue = _presetDefaultValues.TryGetValue(bodyTypeLabel, out string bodyShapeValue)
|
|
? bodyShapeValue
|
|
: bodyShapeDefaultValue;
|
|
}
|
|
|
|
bodyShapeNames.Sort();
|
|
|
|
tooltipText = "Select a body preset for you character - a body type preset is made up of combinations of body type, size and musculature.";
|
|
|
|
dropdowns[bodyTypeLabel] = CreatePresetRow(presetsFoldout, bodyTypeLabel, tooltipText, bodyShapeNames, false, bodyShapeDefaultValue, PresetDropdownType.Body);
|
|
|
|
Label colorTitle = new Label("Colors")
|
|
{
|
|
style =
|
|
{
|
|
unityFontStyleAndWeight = new StyleEnum<FontStyle>(FontStyle.Bold)
|
|
},
|
|
tooltip = "Collections of character colors ie. Skin color, teeth color, eye color, hair color that make up the characters colors"
|
|
};
|
|
|
|
SidekickSpecies unrestrictedSpecies = SidekickSpecies.GetByName(_dbManager, "Unrestricted");
|
|
|
|
presetsFoldout.Add(colorTitle);
|
|
_currentColorSpeciesPresetDictionary = new Dictionary<string, SidekickColorPreset>();
|
|
_currentColorOutfitsPresetDictionary = new Dictionary<string, SidekickColorPreset>();
|
|
_currentColorAttachmentsPresetDictionary = new Dictionary<string, SidekickColorPreset>();
|
|
_currentColorMaterialsPresetDictionary = new Dictionary<string, SidekickColorPreset>();
|
|
_currentColorElementsPresetDictionary = new Dictionary<string, SidekickColorPreset>();
|
|
foreach (ColorGroup colorGroup in Enum.GetValues(typeof(ColorGroup)))
|
|
{
|
|
// TODO: remove when Element colors are re-added
|
|
if (colorGroup == ColorGroup.Elements)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
List<SidekickColorPreset> colorPresets = colorGroup is ColorGroup.Species && _currentSpecies.ID != unrestrictedSpecies.ID
|
|
? SidekickColorPreset.GetAllByColorGroupAndSpecies(_dbManager, colorGroup, _currentSpecies)
|
|
: SidekickColorPreset.GetAllByColorGroup(_dbManager, colorGroup);
|
|
|
|
List<string> colorPresetNames = colorPresets.Select(cp => cp.Name).ToList();
|
|
for (int i = 0; i < colorPresetNames.Count; i++)
|
|
{
|
|
switch (colorGroup)
|
|
{
|
|
case ColorGroup.Species:
|
|
_currentColorSpeciesPresetDictionary.Add(colorPresetNames[i], colorPresets[i]);
|
|
tooltipText = "Select a species color preset for your character - a species color preset is made up of the colors that would make up the character if it had no outfit on. (for example - skin, teeth, tongue, fingernails etc)";
|
|
break;
|
|
case ColorGroup.Outfits:
|
|
_currentColorOutfitsPresetDictionary.Add(colorPresetNames[i], colorPresets[i]);
|
|
tooltipText = "Select an outfit color preset for your character - an outfit color preset is made up of the colors that make up the clothing on the character. (for example - torso outfit, arm outfit, hand outfit etc)";
|
|
break;
|
|
case ColorGroup.Attachments:
|
|
_currentColorAttachmentsPresetDictionary.Add(colorPresetNames[i], colorPresets[i]);
|
|
tooltipText = "Select an attachments color preset for your character - an attachments color preset is made up of the colors used on additional parts on the character. (for example - shoulder attachments, back attachments, hip attachments etc)";
|
|
break;
|
|
case ColorGroup.Materials:
|
|
_currentColorMaterialsPresetDictionary.Add(colorPresetNames[i], colorPresets[i]);
|
|
tooltipText = "Select a materials color preset for your character - a materials color preset is made up of the colors that make up general materials of the outfit and attachments. (for example - metal, wood, leather, plastic, bone etc)";
|
|
break;
|
|
case ColorGroup.Elements:
|
|
_currentColorElementsPresetDictionary.Add(colorPresetNames[i], colorPresets[i]);
|
|
break;
|
|
}
|
|
}
|
|
|
|
string defaultValue = "None";
|
|
|
|
if (colorPresetNames.Count > 0)
|
|
{
|
|
defaultValue = _presetDefaultValues.TryGetValue(colorGroup.ToString(), out string value)
|
|
? value
|
|
: defaultValue;
|
|
}
|
|
|
|
colorPresetNames.Sort();
|
|
|
|
if (_processingSpeciesChange && colorGroup == ColorGroup.Species && !_loadingCharacter)
|
|
{
|
|
defaultValue = colorPresetNames.Count > 0 ? colorPresetNames[0] : "None";
|
|
}
|
|
|
|
dropdowns[colorGroup.ToString()] = CreatePresetRow(presetsFoldout, colorGroup.ToString(), tooltipText, colorPresetNames, true, defaultValue, PresetDropdownType.Color);
|
|
}
|
|
|
|
/* TODO: When decals are added (issue 1108), uncomment and update this section
|
|
Label textureTitle = new Label("Textures")
|
|
{
|
|
style =
|
|
{
|
|
unityFontStyleAndWeight = new StyleEnum<FontStyle>(FontStyle.Bold)
|
|
}
|
|
};
|
|
|
|
presetsFoldout.Add(textureTitle);
|
|
CreatePresetRow(presetsFoldout, "Skin", new List<string>(), true, "None", PresetDropdownType.Texture);
|
|
CreatePresetRow(presetsFoldout, "Outfit", new List<string>(), true, "None", PresetDropdownType.Texture);
|
|
*/
|
|
|
|
generateButton.clickable.clicked += delegate
|
|
{
|
|
_applyingPreset = true;
|
|
foreach (PopupField<string> dropdown in dropdowns.Values)
|
|
{
|
|
List<string> values = dropdown.choices;
|
|
values.Remove("None");
|
|
string newValue = "None";
|
|
if (values.Count > 0)
|
|
{
|
|
newValue = values[Random.Range(0, values.Count - 1)];
|
|
}
|
|
dropdown.value = newValue;
|
|
}
|
|
|
|
// if (_newModel != null)
|
|
// {
|
|
// DestroyImmediate(_newModel);
|
|
// }
|
|
|
|
_newModel = GenerateCharacter(false, true);
|
|
UpdatePartUVData();
|
|
_applyingPreset = false;
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Populates the preset part dropdowns.
|
|
/// </summary>
|
|
/// <param name="dropdowns">The list of all preset dropdowns to put the preset part dropdowns into once populated
|
|
/// </param>
|
|
private void PopulatePresetPartDropdowns(Dictionary<string, PopupField<string>> dropdowns)
|
|
{
|
|
_presetPartContainer.Clear();
|
|
string tooltipText = "";
|
|
_currentHeadPresetDictionary = new Dictionary<string, SidekickPartPreset>();
|
|
_currentUpperBodyPresetDictionary = new Dictionary<string, SidekickPartPreset>();
|
|
_currentLowerBodyPresetDictionary = new Dictionary<string, SidekickPartPreset>();
|
|
HashSet<SidekickPartPreset> mappedPresets = new HashSet<SidekickPartPreset>();
|
|
|
|
if (_partPresetFilterToggleMap.Count > 0)
|
|
{
|
|
foreach (KeyValuePair<SidekickPresetFilter, bool> entry in _partPresetFilterToggleMap)
|
|
{
|
|
if (entry.Value)
|
|
{
|
|
if (_sidekickRuntime.MappedPresetFilterDictionary.TryGetValue(entry.Key.Term, out List<SidekickPartPreset> presets))
|
|
{
|
|
mappedPresets.UnionWith(presets);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (_sidekickRuntime.MappedBasePresetDictionary.TryGetValue(_currentSpecies, out List<SidekickPartPreset> basePresets))
|
|
{
|
|
mappedPresets.UnionWith(basePresets);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
mappedPresets.UnionWith(_availablePresets);
|
|
}
|
|
|
|
List<SidekickPartPreset> allPresets = mappedPresets.ToList();
|
|
|
|
SidekickSpecies unrestrictedSpecies = SidekickSpecies.GetByName(_dbManager, "Unrestricted");
|
|
|
|
foreach (PartGroup partGroup in Enum.GetValues(typeof(PartGroup)))
|
|
{
|
|
// only filter head part presets by species
|
|
List<SidekickPartPreset> presets = partGroup is PartGroup.Head && _currentSpecies.ID != unrestrictedSpecies?.ID
|
|
? allPresets.Where(preset => preset.Species.ID == _currentSpecies.ID && preset.PartGroup == partGroup).ToList()
|
|
: allPresets.Where(preset => preset.PartGroup == partGroup).ToList();
|
|
List<string> presetNames = new List<string>();
|
|
foreach (SidekickPartPreset preset in presets)
|
|
{
|
|
switch (partGroup)
|
|
{
|
|
case PartGroup.Head:
|
|
_currentHeadPresetDictionary.Add(preset.Name, preset);
|
|
tooltipText = "Select a head preset for you character - a head preset is made up of parts like a head, nose, eyes and teeth etc.";
|
|
break;
|
|
case PartGroup.UpperBody:
|
|
_currentUpperBodyPresetDictionary.Add(preset.Name, preset);
|
|
tooltipText = "Select an upper body preset for you character - an upper body preset is made up of parts like a torso, arms, hands and a back attachment etc.";
|
|
break;
|
|
case PartGroup.LowerBody:
|
|
_currentLowerBodyPresetDictionary.Add(preset.Name, preset);
|
|
tooltipText = "Select a lower body preset for you character - a lower body preset is made up of parts like hips, legs, feet and hip attachments etc.";
|
|
break;
|
|
}
|
|
|
|
presetNames.Add(preset.Name);
|
|
}
|
|
|
|
string defaultValue = "None";
|
|
|
|
if (presetNames.Count > 0)
|
|
{
|
|
if (_presetDefaultValues.TryGetValue(partGroup.ToString(), out string value))
|
|
{
|
|
defaultValue = presetNames.Contains(value) ? value : "None";
|
|
|
|
if (_processingSpeciesChange && partGroup == PartGroup.Head && value != "None" && defaultValue == "None")
|
|
{
|
|
defaultValue = presetNames[Random.Range(0, presetNames.Count - 1)];
|
|
}
|
|
}
|
|
}
|
|
|
|
presetNames.Sort();
|
|
|
|
dropdowns[partGroup.ToString()] = CreatePresetRow(_presetPartContainer, partGroup.ToString(), tooltipText, presetNames, true, defaultValue, PresetDropdownType.Part);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a selection row for the preset tab with the given values.
|
|
/// </summary>
|
|
/// <param name="view">The view to add the row to.</param>
|
|
/// <param name="rowLabel">The label to put in the row.</param>
|
|
/// <param name="tooltipText">The tooltip to display for this row.</param>
|
|
/// <param name="dropdownValues">The values for the dropdown in the row.</param>
|
|
/// <param name="includeNoneValue">Whether to include `None` as a value in the dropdown.</param>
|
|
/// <param name="defaultValue">The default value to select from the dropdown.</param>
|
|
/// <param name="dropdownType">What section of the preset UI this dropdown is part of.</param>
|
|
/// <returns>The dropdown selection UI element.</returns>
|
|
private PopupField<string> CreatePresetRow(
|
|
VisualElement view,
|
|
string rowLabel,
|
|
string tooltipText,
|
|
List<string> dropdownValues,
|
|
bool includeNoneValue,
|
|
string defaultValue,
|
|
PresetDropdownType dropdownType)
|
|
{
|
|
VisualElement partContainer = new VisualElement
|
|
{
|
|
style =
|
|
{
|
|
minHeight = 20,
|
|
display = DisplayStyle.Flex,
|
|
flexDirection = FlexDirection.Row,
|
|
marginBottom = 2,
|
|
marginTop = 2,
|
|
marginLeft = 15,
|
|
marginRight = 2,
|
|
unityFontStyleAndWeight = new StyleEnum<FontStyle>(FontStyle.Normal)
|
|
}
|
|
};
|
|
|
|
Label partTypeTitle = new Label(rowLabel.ToString())
|
|
{
|
|
style =
|
|
{
|
|
unityTextAlign = TextAnchor.MiddleLeft,
|
|
width = 150
|
|
},
|
|
tooltip = tooltipText
|
|
};
|
|
|
|
Button removeButton = new Button()
|
|
{
|
|
tooltip = "Remove this preset, resets selection to None"
|
|
};
|
|
|
|
removeButton.Add(
|
|
new Image
|
|
{
|
|
image = Resources.Load<Texture2D>("UI/T_Clear"),
|
|
scaleMode = ScaleMode.ScaleToFit
|
|
}
|
|
);
|
|
|
|
int marginLeft = 0;
|
|
if (dropdownType != PresetDropdownType.Part)
|
|
{
|
|
marginLeft = 36;
|
|
}
|
|
|
|
Button previousButton = new Button()
|
|
{
|
|
tooltip = "Select the previous preset",
|
|
style =
|
|
{
|
|
marginLeft = marginLeft
|
|
}
|
|
};
|
|
|
|
previousButton.Add(
|
|
new Image
|
|
{
|
|
image = EditorGUIUtility.IconContent("tab_prev", "|Previous Preset").image,
|
|
scaleMode = ScaleMode.ScaleToFit
|
|
}
|
|
);
|
|
|
|
Button nextButton = new Button()
|
|
{
|
|
tooltip = "Select the next preset"
|
|
};
|
|
|
|
nextButton.Add(
|
|
new Image
|
|
{
|
|
image = EditorGUIUtility.IconContent("tab_next", "|Next Preset").image,
|
|
scaleMode = ScaleMode.ScaleToFit
|
|
}
|
|
);
|
|
|
|
Button randomButton = new Button()
|
|
{
|
|
tooltip = "Randomly select a preset"
|
|
};
|
|
|
|
Texture2D randomImage = Resources.Load<Texture2D>("UI/T_Random");
|
|
randomButton.Add(
|
|
new Image
|
|
{
|
|
image = randomImage,
|
|
scaleMode = ScaleMode.ScaleToFit,
|
|
style =
|
|
{
|
|
paddingTop = new StyleLength(1),
|
|
paddingBottom = new StyleLength(1)
|
|
}
|
|
}
|
|
);
|
|
|
|
if (!dropdownValues.Contains(defaultValue))
|
|
{
|
|
defaultValue = includeNoneValue ? "None" : dropdownValues[0];
|
|
}
|
|
|
|
List<string> popupValues = new List<string>();
|
|
if (includeNoneValue)
|
|
{
|
|
popupValues.Add("None");
|
|
};
|
|
|
|
popupValues.AddRange(dropdownValues);
|
|
|
|
PopupField<string> partSelection = new PopupField<string>(popupValues, 0)
|
|
{
|
|
value = "None",
|
|
style =
|
|
{
|
|
minWidth = 180
|
|
},
|
|
tooltip = tooltipText
|
|
};
|
|
|
|
partSelection.RegisterValueChangedCallback(
|
|
evt =>
|
|
{
|
|
_presetDefaultValues[rowLabel] = evt.newValue;
|
|
|
|
// Correctly enable/disable next and previous buttons based on selection
|
|
previousButton.SetEnabled(partSelection.index > 0);
|
|
nextButton.SetEnabled(partSelection.index < popupValues.Count - 1);
|
|
|
|
switch (dropdownType)
|
|
{
|
|
case PresetDropdownType.Part:
|
|
_applyingPreset = true;
|
|
|
|
bool hasErrors = false;
|
|
string errorMessage = "The following parts could not be found in your project:\n";
|
|
Enum.TryParse(rowLabel, out PartGroup group);
|
|
List<CharacterPartType> partTypesToRemove = group.GetPartTypes();
|
|
SidekickPartPreset currentPartPreset = null;
|
|
List<SidekickPartPresetRow> presetParts = new List<SidekickPartPresetRow>();
|
|
// NOTE : need to ensure evt.newValue is always in the dictionary ahead of this, or change to GetValueOrDefault()
|
|
if (evt.newValue != "None")
|
|
{
|
|
switch (group)
|
|
{
|
|
case PartGroup.Head:
|
|
currentPartPreset = _currentHeadPresetDictionary[evt.newValue];
|
|
break;
|
|
case PartGroup.UpperBody:
|
|
currentPartPreset = _currentUpperBodyPresetDictionary[evt.newValue];
|
|
break;
|
|
case PartGroup.LowerBody:
|
|
currentPartPreset = _currentLowerBodyPresetDictionary[evt.newValue];
|
|
break;
|
|
}
|
|
presetParts = SidekickPartPresetRow.GetAllByPreset(_dbManager, currentPartPreset);
|
|
}
|
|
|
|
foreach (SidekickPartPresetRow presetPart in presetParts)
|
|
{
|
|
if (Enum.TryParse(CharacterPartTypeUtils.GetTypeNameFromShortcode(presetPart.PartType), out CharacterPartType partType))
|
|
{
|
|
if (_partSelectionDictionary.TryGetValue(partType, out PartTypeControls currentField))
|
|
{
|
|
if (presetPart.Part != null)
|
|
{
|
|
_currentCharacter[partType] = presetPart.Part;
|
|
}
|
|
|
|
UpdateResult result = new UpdateResult(errorMessage, hasErrors);
|
|
|
|
if (partType == CharacterPartType.Wrap)
|
|
{
|
|
if (_partSelectionDictionary.TryGetValue(partType, out PartTypeControls wrapSelection))
|
|
{
|
|
if (_requiresWrap && _bodyTypeBlendValue > 0)
|
|
{
|
|
wrapSelection.PartDropdown.SetEnabled(true);
|
|
wrapSelection.RandomisePartDropdownValue();
|
|
}
|
|
else
|
|
{
|
|
wrapSelection.PartDropdown.SetEnabled(false);
|
|
wrapSelection.SetPartDropdownValue(null);
|
|
_currentCharacter.Remove(CharacterPartType.Wrap);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
result = UpdatePartDropdown(
|
|
currentField,
|
|
presetPart.Part?.Name ?? "None",
|
|
errorMessage,
|
|
hasErrors
|
|
);
|
|
}
|
|
|
|
hasErrors = result.HasErrors;
|
|
errorMessage = result.ErrorMessage;
|
|
partTypesToRemove.Remove(partType);
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach (CharacterPartType partType in partTypesToRemove)
|
|
{
|
|
if (_partSelectionDictionary.TryGetValue(partType, out PartTypeControls currentField))
|
|
{
|
|
UpdateResult result = UpdatePartDropdown(currentField, "None", errorMessage, hasErrors);
|
|
|
|
hasErrors = result.HasErrors;
|
|
errorMessage = result.ErrorMessage;
|
|
}
|
|
}
|
|
|
|
if (hasErrors)
|
|
{
|
|
EditorUtility.DisplayDialog(
|
|
"Assets Missing",
|
|
errorMessage,
|
|
"OK"
|
|
);
|
|
}
|
|
|
|
_applyingPreset = false;
|
|
|
|
_newModel = GameObject.Find(_OUTPUT_MODEL_NAME);
|
|
|
|
// if (_newModel != null)
|
|
// {
|
|
// DestroyImmediate(_newModel);
|
|
// }
|
|
|
|
_newModel = GenerateCharacter(false, true);
|
|
UpdatePartUVData();
|
|
break;
|
|
case PresetDropdownType.Body:
|
|
SidekickBodyShapePreset bodyShapePreset = _currentBodyPresetDictionary[evt.newValue];
|
|
if (Math.Abs(_bodyTypeSlider.value - bodyShapePreset.BodyType) < 0.001f)
|
|
{
|
|
string body = "None";
|
|
if (_partSelectionDictionary.TryGetValue(CharacterPartType.Torso, out PartTypeControls bodySelection))
|
|
{
|
|
body = bodySelection.PartDropdown.value;
|
|
}
|
|
|
|
if (_partSelectionDictionary.TryGetValue(CharacterPartType.Wrap, out PartTypeControls wrapSelection))
|
|
{
|
|
if (body != "None" && _requiresWrap && _bodyTypeBlendValue > 0)
|
|
{
|
|
wrapSelection.PartDropdown.SetEnabled(true);
|
|
wrapSelection.RandomisePartDropdownValue();
|
|
}
|
|
else
|
|
{
|
|
wrapSelection.PartDropdown.SetEnabled(false);
|
|
wrapSelection.SetPartDropdownValue(null);
|
|
_currentCharacter.Remove(CharacterPartType.Wrap);
|
|
}
|
|
}
|
|
}
|
|
|
|
_bodyTypeSlider.value = bodyShapePreset.BodyType;
|
|
_bodySizeSlider.value = bodyShapePreset.BodySize;
|
|
_musclesSlider.value = bodyShapePreset.Musculature;
|
|
break;
|
|
case PresetDropdownType.Color:
|
|
if (evt.newValue == "None")
|
|
{
|
|
return;
|
|
}
|
|
|
|
ResetCurrentColorSet();
|
|
|
|
Enum.TryParse(rowLabel, out ColorGroup colorGroup);
|
|
SidekickColorPreset colorPreset = null;
|
|
// NOTE : need to ensure evt.newValue is always in the dictionary ahead of this, or change to GetValueOrDefault()
|
|
switch (colorGroup)
|
|
{
|
|
case ColorGroup.Species:
|
|
colorPreset = _currentColorSpeciesPresetDictionary[evt.newValue];
|
|
break;
|
|
case ColorGroup.Outfits:
|
|
colorPreset = _currentColorOutfitsPresetDictionary[evt.newValue];
|
|
break;
|
|
case ColorGroup.Attachments:
|
|
colorPreset = _currentColorAttachmentsPresetDictionary[evt.newValue];
|
|
break;
|
|
case ColorGroup.Materials:
|
|
colorPreset = _currentColorMaterialsPresetDictionary[evt.newValue];
|
|
break;/*
|
|
case ColorGroup.Elements:
|
|
colorPreset = _currentColorElementsPresetDictionary[evt.newValue];
|
|
break;*/
|
|
}
|
|
|
|
List<SidekickColorPresetRow> presetColorRows = SidekickColorPresetRow.GetAllByPreset(_dbManager, colorPreset);
|
|
foreach (SidekickColorPresetRow row in presetColorRows)
|
|
{
|
|
SidekickColorRow existingRow = _allColorRows.Find(r => r.ColorProperty.ID == row.ColorProperty.ID);
|
|
if (existingRow == null)
|
|
{
|
|
existingRow = new SidekickColorRow()
|
|
{
|
|
ID = -1,
|
|
ColorSet = _currentColorSet,
|
|
ColorProperty = row.ColorProperty,
|
|
NiceColor = row.NiceColor,
|
|
NiceMetallic = row.NiceMetallic,
|
|
NiceSmoothness = row.NiceSmoothness,
|
|
NiceReflection = row.NiceReflection,
|
|
NiceEmission = row.NiceEmission,
|
|
NiceOpacity = row.NiceOpacity
|
|
};
|
|
|
|
_allColorRows.Add(existingRow);
|
|
}
|
|
else
|
|
{
|
|
existingRow.NiceColor = row.NiceColor;
|
|
existingRow.NiceMetallic = row.NiceMetallic;
|
|
existingRow.NiceSmoothness = row.NiceSmoothness;
|
|
existingRow.NiceReflection = row.NiceReflection;
|
|
existingRow.NiceEmission = row.NiceEmission;
|
|
existingRow.NiceOpacity = row.NiceOpacity;
|
|
}
|
|
|
|
UpdateAllColors(existingRow);
|
|
}
|
|
|
|
PopulatePartColorRows();
|
|
RefreshVisibleColorRows();
|
|
break;
|
|
case PresetDropdownType.Texture:
|
|
// TODO: Add texture setting functionality once decal system in place
|
|
break;
|
|
default:
|
|
throw new ArgumentOutOfRangeException(nameof(dropdownType), dropdownType, null);
|
|
}
|
|
}
|
|
);
|
|
|
|
removeButton.clickable.clicked += delegate
|
|
{
|
|
// TODO: Change to default character when available
|
|
partSelection.value = "None";
|
|
};
|
|
|
|
previousButton.clickable.clicked += delegate
|
|
{
|
|
int newIndex = partSelection.index - 1;
|
|
if (newIndex <= 0)
|
|
{
|
|
newIndex = 0;
|
|
}
|
|
|
|
partSelection.index = newIndex;
|
|
};
|
|
|
|
nextButton.clickable.clicked += delegate
|
|
{
|
|
int newIndex = partSelection.index + 1;
|
|
if (newIndex >= partSelection.choices.Count - 1)
|
|
{
|
|
newIndex = partSelection.choices.Count - 1;
|
|
}
|
|
|
|
partSelection.index = newIndex;
|
|
};
|
|
|
|
randomButton.clickable.clicked += delegate
|
|
{
|
|
int currentIndex = partSelection.index;
|
|
if (partSelection.choices.Count - 1 > 1)
|
|
{
|
|
while (partSelection.index == currentIndex)
|
|
{
|
|
partSelection.index = Random.Range(1, partSelection.choices.Count);
|
|
}
|
|
}
|
|
};
|
|
|
|
partContainer.Add(partTypeTitle);
|
|
if (dropdownType == PresetDropdownType.Part)
|
|
{
|
|
partContainer.Add(removeButton);
|
|
}
|
|
partContainer.Add(previousButton);
|
|
partContainer.Add(nextButton);
|
|
partContainer.Add(randomButton);
|
|
partContainer.Add(partSelection);
|
|
view.Add(partContainer);
|
|
|
|
partSelection.value = defaultValue;
|
|
|
|
return partSelection;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the required parts of the UI on a species change.
|
|
/// </summary>
|
|
/// <param name="newSpecies">The name of the species being changed to.</param>
|
|
private void ProcessSpeciesChange(string newSpecies)
|
|
{
|
|
// don't need to re-process if multiple callbacks are triggered
|
|
if (_currentSpecies.Name == newSpecies)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_currentSpecies = _allSpecies.FirstOrDefault(species => species.Name == newSpecies);
|
|
_sidekickRuntime.CurrentSpecies = _currentSpecies;
|
|
UpdateVisibleColorSets();
|
|
ResetCurrentColorSet();
|
|
if (_allColorRows.Count == 0)
|
|
{
|
|
PopulateColorRowsFromTextures();
|
|
}
|
|
|
|
_appliedPartFilters.ResetFiltersForSpeciesChange();
|
|
|
|
_processingSpeciesChange = true;
|
|
UpdatePartDropdowns();
|
|
PopulatePresetUI();
|
|
_processingSpeciesChange = false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Populates the parts UI.
|
|
/// </summary>
|
|
private void PopulatePartUI()
|
|
{
|
|
_partView.Clear();
|
|
|
|
_availablePartList = new List<SidekickPart>();
|
|
_partSelectionDictionary = new Dictionary<CharacterPartType, PartTypeControls>();
|
|
_partLockMap = new Dictionary<PopupField<string>, bool>();
|
|
|
|
Foldout speciesFoldout = new Foldout
|
|
{
|
|
text = "Select - Species",
|
|
style =
|
|
{
|
|
unityFontStyleAndWeight = new StyleEnum<FontStyle>(FontStyle.Bold)
|
|
}
|
|
};
|
|
|
|
List<string> speciesNames = _allSpecies.Select(species => species.Name).ToList();
|
|
|
|
_speciesField = new DropdownField
|
|
{
|
|
label = "Species",
|
|
style =
|
|
{
|
|
unityFontStyleAndWeight = new StyleEnum<FontStyle>(FontStyle.Normal)
|
|
},
|
|
tooltip = "Select the species of your character"
|
|
};
|
|
_speciesField.choices = speciesNames;
|
|
_speciesField.RegisterValueChangedCallback(
|
|
evt =>
|
|
{
|
|
_speciesPresetField.value = _speciesField.value;
|
|
ProcessSpeciesChange(evt.newValue);
|
|
}
|
|
);
|
|
|
|
_speciesField.index = _currentSpecies != null && speciesNames.Count > 0 ? _speciesField.choices.IndexOf(_currentSpecies.Name) : 0;
|
|
speciesFoldout.Add(_speciesField);
|
|
_partView.Add(speciesFoldout);
|
|
|
|
_partsFoldout = new Foldout()
|
|
{
|
|
text = "Select - Parts",
|
|
style =
|
|
{
|
|
unityFontStyleAndWeight = new StyleEnum<FontStyle>(FontStyle.Bold)
|
|
}
|
|
};
|
|
|
|
List<SidekickPartFilter> outfitFilters = SidekickPartFilter.GetAllForFilterType(_dbManager, FilterType.Outfit);
|
|
List<SidekickPartFilter> orderedFilters = new List<SidekickPartFilter>();
|
|
|
|
outfitFilters.Sort(
|
|
(filterA, filterB) => String.CompareOrdinal(filterA.Term, filterB.Term)
|
|
);
|
|
|
|
orderedFilters.AddRange(outfitFilters);
|
|
|
|
Foldout filterFoldout = new Foldout()
|
|
{
|
|
text = "Select - Outfit Filter",
|
|
style =
|
|
{
|
|
unityFontStyleAndWeight = new StyleEnum<FontStyle>(FontStyle.Bold)
|
|
}
|
|
};
|
|
|
|
_appliedPartFilters = new FilterGroup()
|
|
{
|
|
Runtime = _sidekickRuntime,
|
|
CombineType = FilterCombineType.Or
|
|
};
|
|
|
|
VisualElement toggleList = new VisualElement()
|
|
{
|
|
style =
|
|
{
|
|
flexDirection = new StyleEnum<FlexDirection>(FlexDirection.Row),
|
|
flexWrap = new StyleEnum<Wrap>(Wrap.Wrap)
|
|
}
|
|
};
|
|
|
|
List<Toggle> allFilterToggles = new List<Toggle>();
|
|
|
|
Color borderColor = new Color(0.17f, 0.17f, 0.17f);
|
|
Color backgroundColor = new Color(0.35f, 0.35f, 0.35f);
|
|
|
|
foreach (SidekickPartFilter filter in orderedFilters)
|
|
{
|
|
Toggle outfitToggle = new Toggle(filter.Term)
|
|
{
|
|
value = true,
|
|
style =
|
|
{
|
|
width = 160,
|
|
borderBottomWidth = 1,
|
|
borderBottomColor = borderColor,
|
|
paddingBottom = 2,
|
|
borderLeftWidth = 1,
|
|
borderLeftColor = borderColor,
|
|
paddingLeft = 2,
|
|
borderRightWidth = 1,
|
|
borderRightColor = borderColor,
|
|
paddingRight = 2,
|
|
borderTopWidth = 1,
|
|
borderTopColor = borderColor,
|
|
paddingTop = 2,
|
|
borderBottomLeftRadius = 3,
|
|
borderBottomRightRadius = 3,
|
|
borderTopLeftRadius = 3,
|
|
borderTopRightRadius = 3,
|
|
backgroundColor = backgroundColor,
|
|
textOverflow = new StyleEnum<TextOverflow>(TextOverflow.Ellipsis)
|
|
}
|
|
};
|
|
|
|
FilterItem filterItem = new FilterItem(_sidekickRuntime, filter, FilterCombineType.Or);
|
|
_appliedPartFilters.AddFilterItem(filterItem);
|
|
|
|
outfitToggle.RegisterValueChangedCallback(
|
|
evt =>
|
|
{
|
|
if (evt.newValue)
|
|
{
|
|
_appliedPartFilters.AddFilterItem(filterItem);
|
|
}
|
|
else
|
|
{
|
|
_appliedPartFilters.RemoveFilterItem(filterItem);
|
|
}
|
|
UpdatePartDropdowns();
|
|
}
|
|
);
|
|
|
|
toggleList.Add(outfitToggle);
|
|
allFilterToggles.Add(outfitToggle);
|
|
}
|
|
|
|
VisualElement buttonRow = new VisualElement()
|
|
{
|
|
style =
|
|
{
|
|
flexDirection = new StyleEnum<FlexDirection>(FlexDirection.Row)
|
|
}
|
|
};
|
|
|
|
Button selectAll = new Button(
|
|
delegate
|
|
{
|
|
foreach (Toggle toggle in allFilterToggles)
|
|
{
|
|
toggle.value = true;
|
|
}
|
|
}
|
|
)
|
|
{
|
|
text = "Select All"
|
|
};
|
|
|
|
Button selectNone = new Button(
|
|
delegate
|
|
{
|
|
foreach (Toggle toggle in allFilterToggles)
|
|
{
|
|
toggle.value = false;
|
|
}
|
|
}
|
|
)
|
|
{
|
|
text = "Select None"
|
|
};
|
|
|
|
buttonRow.Add(selectAll);
|
|
buttonRow.Add(selectNone);
|
|
|
|
filterFoldout.Add(buttonRow);
|
|
filterFoldout.Add(toggleList);
|
|
|
|
_partView.Add(filterFoldout);
|
|
|
|
foreach (PartGroup partGroup in Enum.GetValues(typeof(PartGroup)))
|
|
{
|
|
string labelText = StringUtils.AddSpacesBeforeCapitalLetters(partGroup.ToString());
|
|
Foldout partGroupFoldout = new Foldout
|
|
{
|
|
text = labelText,
|
|
style =
|
|
{
|
|
marginLeft = 15,
|
|
unityFontStyleAndWeight = new StyleEnum<FontStyle>(FontStyle.Bold)
|
|
}
|
|
};
|
|
|
|
Button randomiseAllButton = new Button()
|
|
{
|
|
text = "Randomize " + labelText
|
|
};
|
|
|
|
partGroupFoldout.Add(randomiseAllButton);
|
|
|
|
foreach (CharacterPartType value in partGroup.GetPartTypes())
|
|
{
|
|
VisualElement partContainer = new VisualElement
|
|
{
|
|
style =
|
|
{
|
|
minHeight = 20,
|
|
display = DisplayStyle.Flex,
|
|
flexDirection = FlexDirection.Row,
|
|
marginBottom = 2,
|
|
marginTop = 2,
|
|
marginRight = 2,
|
|
unityFontStyleAndWeight = new StyleEnum<FontStyle>(FontStyle.Normal)
|
|
}
|
|
};
|
|
BuildPartDetails(value, partContainer);
|
|
partGroupFoldout.Add(partContainer);
|
|
}
|
|
|
|
_partsFoldout.Add(partGroupFoldout);
|
|
|
|
randomiseAllButton.clickable.clicked += delegate
|
|
{
|
|
foreach (CharacterPartType value in partGroup.GetPartTypes())
|
|
{
|
|
PartTypeControls dropdown = _partSelectionDictionary[value];
|
|
bool locked = _partLockMap[dropdown.PartDropdown];
|
|
if (!locked)
|
|
{
|
|
if (dropdown.PartDropdown.choices.Count > 1)
|
|
{
|
|
dropdown.RandomisePartDropdownValue();
|
|
}
|
|
else
|
|
{
|
|
dropdown.SetPartDropdownValue("None");
|
|
}
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
UpdatePartDropdowns();
|
|
|
|
_partView.Add(_partsFoldout);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Builds the UI details for each different part type.
|
|
/// </summary>
|
|
/// <param name="type">The type of the part to build the UI for.</param>
|
|
/// <param name="partContainer">The container to add the UI to.</param>
|
|
private void BuildPartDetails(CharacterPartType type, VisualElement partContainer)
|
|
{
|
|
List<SidekickPart> partsList = new List<SidekickPart>();
|
|
|
|
foreach (SidekickPart part in _allPartsLibrary[type])
|
|
{
|
|
if (_availablePartList.Any(p => p.ID == part.ID))
|
|
{
|
|
partsList.Add(part);
|
|
}
|
|
}
|
|
|
|
Label partTypeTitle = new Label(type.ToString())
|
|
{
|
|
style =
|
|
{
|
|
unityTextAlign = TextAnchor.MiddleLeft,
|
|
width = 150
|
|
},
|
|
tooltip = type.GetTooltipForPartType()
|
|
};
|
|
|
|
Image lockImage = new Image
|
|
{
|
|
image = EditorGUIUtility.IconContent("LockIcon", "|Lock Part").image,
|
|
scaleMode = ScaleMode.ScaleToFit,
|
|
style =
|
|
{
|
|
alignSelf = new StyleEnum<Align>(Align.Center),
|
|
width = 15,
|
|
height = 15,
|
|
paddingTop = 2
|
|
}
|
|
};
|
|
|
|
Button lockButton = new Button()
|
|
{
|
|
tooltip = "Remove this part"
|
|
};
|
|
|
|
lockButton.Add(
|
|
lockImage
|
|
);
|
|
|
|
Button removeButton = new Button()
|
|
{
|
|
tooltip = "Remove this part"
|
|
};
|
|
|
|
removeButton.Add(
|
|
new Image
|
|
{
|
|
image = Resources.Load<Texture2D>("UI/T_Clear"),
|
|
scaleMode = ScaleMode.ScaleToFit
|
|
}
|
|
);
|
|
|
|
Button previousButton = new Button()
|
|
{
|
|
tooltip = "Select the previous part"
|
|
};
|
|
|
|
previousButton.Add(
|
|
new Image
|
|
{
|
|
image = EditorGUIUtility.IconContent("tab_prev", "|Previous Part").image,
|
|
scaleMode = ScaleMode.ScaleToFit
|
|
}
|
|
);
|
|
|
|
Button nextButton = new Button()
|
|
{
|
|
tooltip = "Select the next part"
|
|
};
|
|
|
|
nextButton.Add(
|
|
new Image
|
|
{
|
|
image = EditorGUIUtility.IconContent("tab_next", "|Next Part").image,
|
|
scaleMode = ScaleMode.ScaleToFit
|
|
}
|
|
);
|
|
|
|
Button randomButton = new Button()
|
|
{
|
|
tooltip = "Randomly select a part"
|
|
};
|
|
|
|
Texture2D randomImage = Resources.Load<Texture2D>("UI/T_Random");
|
|
randomButton.Add(
|
|
new Image
|
|
{
|
|
image = randomImage,
|
|
scaleMode = ScaleMode.ScaleToFit,
|
|
style =
|
|
{
|
|
paddingTop = new StyleLength(1),
|
|
paddingBottom = new StyleLength(1)
|
|
}
|
|
}
|
|
);
|
|
|
|
List<string> popupValues = new List<string>();
|
|
|
|
if (type != CharacterPartType.Wrap)
|
|
{
|
|
popupValues.Add("None");
|
|
}
|
|
|
|
foreach (SidekickPart part in partsList)
|
|
{
|
|
SidekickPartSpeciesLink link = SidekickPartSpeciesLink.GetForSpeciesAndPart(_dbManager, _currentSpecies, part);
|
|
if (link != null)
|
|
{
|
|
popupValues.Add(part.Name);
|
|
}
|
|
}
|
|
|
|
_currentCharacter.TryGetValue(type, out SidekickPart selectedPart);
|
|
string currentSelection = selectedPart?.Name ?? "None";
|
|
if (_processingSpeciesChange)
|
|
{
|
|
if (popupValues.Count < 1 || (popupValues.Count == 1 && popupValues[0] == "None"))
|
|
{
|
|
_previousPartSelections[type] = currentSelection;
|
|
currentSelection = "None";
|
|
}
|
|
else if (PartUtils.IsBaseSpeciesPart(currentSelection))
|
|
{
|
|
_previousPartSelections[type] = currentSelection;
|
|
currentSelection = popupValues.Find(n => n.Contains("BASE")) ?? "None";
|
|
}
|
|
else if (currentSelection == "None" && _previousPartSelections[type] != "None" && popupValues.Count > 1)
|
|
{
|
|
currentSelection = _previousPartSelections[type];
|
|
_previousPartSelections[type] = "None";
|
|
}
|
|
|
|
if (!popupValues.Contains(currentSelection))
|
|
{
|
|
_previousPartSelections[type] = currentSelection;
|
|
currentSelection = FindClosestPartMatch(popupValues, currentSelection);
|
|
}
|
|
}
|
|
|
|
PopupField<string> partSelection = new PopupField<string>(popupValues, 0)
|
|
{
|
|
value = currentSelection
|
|
};
|
|
|
|
PartTypeControls controls = new PartTypeControls
|
|
{
|
|
PartType = type,
|
|
PartDropdown = partSelection,
|
|
ClearButton = removeButton,
|
|
NextButton = nextButton,
|
|
PreviousButton = previousButton,
|
|
RandomButton = randomButton
|
|
};
|
|
|
|
partSelection.RegisterCallback<ChangeEvent<string>>(
|
|
changeEvent =>
|
|
{
|
|
PartSelectionChangeEvent(changeEvent, type, controls);
|
|
}
|
|
);
|
|
|
|
_partLockMap[partSelection] = false;
|
|
|
|
lockButton.clickable.clicked += delegate
|
|
{
|
|
bool newValue = !_partLockMap[partSelection];
|
|
_partLockMap[partSelection] = newValue;
|
|
|
|
if (newValue)
|
|
{
|
|
partSelection.SetEnabled(false);
|
|
removeButton.SetEnabled(false);
|
|
nextButton.SetEnabled(false);
|
|
previousButton.SetEnabled(false);
|
|
randomButton.SetEnabled(false);
|
|
lockImage.image = EditorGUIUtility.IconContent("LockIcon-On", "|Unlock Part").image;
|
|
lockButton.style.backgroundColor = new Color(0.2f, 0.2f, 0.2f);
|
|
}
|
|
else
|
|
{
|
|
partSelection.SetEnabled(true);
|
|
removeButton.SetEnabled(true);
|
|
nextButton.SetEnabled(true);
|
|
previousButton.SetEnabled(true);
|
|
randomButton.SetEnabled(true);
|
|
PartSelectionChangeEvent(new ChangeEvent<string>(), type, controls);
|
|
lockImage.image = EditorGUIUtility.IconContent("LockIcon", "|Lock Part").image;
|
|
lockButton.style.backgroundColor = new Color(0.345098f, 0.345098f, 0.345098f);
|
|
}
|
|
};
|
|
|
|
if (_processingSpeciesChange)
|
|
{
|
|
ChangeEvent<string> changeEvent = ChangeEvent<string>.GetPooled(_previousPartSelections[type], currentSelection);
|
|
PartSelectionChangeEvent(changeEvent, type, controls);
|
|
changeEvent.Dispose();
|
|
}
|
|
|
|
_partSelectionDictionary.Add(type, controls);
|
|
|
|
removeButton.clickable.clicked += delegate
|
|
{
|
|
partSelection.value = "None";
|
|
};
|
|
|
|
previousButton.clickable.clicked += delegate
|
|
{
|
|
int newIndex = partSelection.index - 1;
|
|
if (newIndex <= 0)
|
|
{
|
|
newIndex = 0;
|
|
}
|
|
|
|
partSelection.index = newIndex;
|
|
};
|
|
|
|
nextButton.clickable.clicked += delegate
|
|
{
|
|
int newIndex = partSelection.index + 1;
|
|
if (newIndex >= partSelection.choices.Count - 1)
|
|
{
|
|
newIndex = partSelection.choices.Count - 1;
|
|
}
|
|
|
|
partSelection.index = newIndex;
|
|
};
|
|
|
|
randomButton.clickable.clicked += delegate
|
|
{
|
|
int currentIndex = partSelection.index;
|
|
if (partSelection.choices.Count - 1 > 1)
|
|
{
|
|
while (partSelection.index == currentIndex)
|
|
{
|
|
partSelection.index = Random.Range(1, partSelection.choices.Count);
|
|
}
|
|
}
|
|
};
|
|
|
|
partContainer.Add(partTypeTitle);
|
|
partContainer.Add(lockButton);
|
|
partContainer.Add(removeButton);
|
|
partContainer.Add(previousButton);
|
|
partContainer.Add(nextButton);
|
|
partContainer.Add(randomButton);
|
|
partContainer.Add(partSelection);
|
|
}
|
|
|
|
private void UpdatePartDropdowns()
|
|
{
|
|
Dictionary<CharacterPartType, List<string>> filteredParts = _appliedPartFilters.GetFilteredParts();
|
|
Dictionary<CharacterPartType, List<string>> baseParts = _sidekickRuntime.MappedBasePartDictionary[_currentSpecies];
|
|
|
|
foreach (CharacterPartType type in Enum.GetValues(typeof(CharacterPartType)))
|
|
{
|
|
PartTypeControls controls = _partSelectionDictionary[type];
|
|
HashSet<string> popupItems = new HashSet<string>();
|
|
|
|
if (type != CharacterPartType.Wrap)
|
|
{
|
|
popupItems.Add("None");
|
|
};
|
|
|
|
HashSet<string> itemList = baseParts.TryGetValue(type, out List<string> items) ? items.ToHashSet() : new HashSet<string>();
|
|
popupItems.UnionWith(itemList);
|
|
itemList = filteredParts.TryGetValue(type, out List<string> filteredItems) ? filteredItems.ToHashSet() : new HashSet<string>();
|
|
popupItems.UnionWith(itemList);
|
|
|
|
List<string> popupValues = popupItems.ToList();
|
|
|
|
_currentCharacter.TryGetValue(type, out SidekickPart selectedPart);
|
|
string currentSelection = selectedPart?.Name ?? "None";
|
|
if (_processingSpeciesChange)
|
|
{
|
|
if (popupValues.Count < 1 || (popupValues.Count == 1 && popupValues[0] == "None"))
|
|
{
|
|
_previousPartSelections[type] = currentSelection;
|
|
currentSelection = "None";
|
|
}
|
|
else if (PartUtils.IsBaseSpeciesPart(currentSelection))
|
|
{
|
|
_previousPartSelections[type] = currentSelection;
|
|
currentSelection = popupValues.Find(n => n.Contains("BASE")) ?? "None";
|
|
}
|
|
else if (currentSelection == "None" && _previousPartSelections[type] != "None" && popupValues.Count > 1)
|
|
{
|
|
currentSelection = _previousPartSelections[type];
|
|
_previousPartSelections[type] = "None";
|
|
}
|
|
|
|
if (!popupValues.Contains(currentSelection))
|
|
{
|
|
_previousPartSelections[type] = currentSelection;
|
|
currentSelection = FindClosestPartMatch(popupValues, currentSelection);
|
|
}
|
|
}
|
|
|
|
if (type == CharacterPartType.Wrap && currentSelection == "None")
|
|
{
|
|
currentSelection = null;
|
|
}
|
|
|
|
controls.UpdateDropdownValues(popupValues);
|
|
controls.SetPartDropdownValue(currentSelection);
|
|
controls.UpdateControls();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Process the change event when a new part is selected.
|
|
/// </summary>
|
|
/// <param name="changeEvent">The change event to process.</param>
|
|
/// <param name="type">The type of part that has been changed.</param>
|
|
/// <param name="partSelection">The UI PopupField the change event is happening for.</param>
|
|
private void PartSelectionChangeEvent(ChangeEvent<string> changeEvent, CharacterPartType type, PartTypeControls partSelection)
|
|
{
|
|
try
|
|
{
|
|
if (_currentTab == TabView.Parts
|
|
&& _sidekickRuntime.MappedPartDictionary.ContainsKey(type)
|
|
&& changeEvent.newValue != null
|
|
&& _sidekickRuntime.MappedPartDictionary[type].TryGetValue(changeEvent.newValue, out SidekickPart selectedPart))
|
|
{
|
|
|
|
GameObject partModel = selectedPart.GetPartModel();
|
|
SkinnedMeshRenderer selectedMesh = partModel.GetComponentInChildren<SkinnedMeshRenderer>();
|
|
_partDictionary[type] = selectedMesh;
|
|
_currentCharacter[type] = selectedPart;
|
|
|
|
if (!_processingSpeciesChange)
|
|
{
|
|
_previousPartSelections[type] = changeEvent.newValue;
|
|
}
|
|
|
|
if (type == CharacterPartType.Torso)
|
|
{
|
|
_requiresWrap = selectedPart.UsesWrap;
|
|
if (_partSelectionDictionary.TryGetValue(CharacterPartType.Wrap, out PartTypeControls wrapSelection))
|
|
{
|
|
if (_requiresWrap)
|
|
{
|
|
wrapSelection.PartDropdown.SetEnabled(true);
|
|
wrapSelection.RandomisePartDropdownValue();
|
|
;
|
|
}
|
|
else
|
|
{
|
|
wrapSelection.PartDropdown.SetEnabled(false);
|
|
wrapSelection.SetPartDropdownValue(null);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (changeEvent.newValue == "None")
|
|
{
|
|
_currentCharacter.Remove(type);
|
|
_partDictionary.Remove(type);
|
|
if (!_processingSpeciesChange)
|
|
{
|
|
_previousPartSelections[type] = "None";
|
|
}
|
|
|
|
if (type == CharacterPartType.Torso)
|
|
{
|
|
if (_partSelectionDictionary.TryGetValue(CharacterPartType.Wrap, out PartTypeControls wrapSelection))
|
|
{
|
|
wrapSelection.PartDropdown.SetEnabled(false);
|
|
wrapSelection.SetPartDropdownValue(null);
|
|
_currentCharacter.Remove(CharacterPartType.Wrap);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
_partDictionary.Remove(type);
|
|
}
|
|
|
|
if (!_applyingPreset && _previewToggle.value)
|
|
{
|
|
// if (_combineMeshes && _newModel != null)
|
|
// {
|
|
// DestroyImmediate(_newModel);
|
|
// }
|
|
|
|
_newModel = GenerateCharacter(false, true);
|
|
bool switchAnimation = SetupAnimationControllers();
|
|
|
|
if (switchAnimation && (type == CharacterPartType.HandLeft || type == CharacterPartType.HandRight))
|
|
{
|
|
SetState("InspectHands");
|
|
}
|
|
|
|
UpdatePartUVData();
|
|
}
|
|
|
|
partSelection.UpdateControls();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
EditorUtility.DisplayDialog("Failed loading part", "Failed to load the following part\n" + changeEvent.newValue, "OK");
|
|
Debug.LogWarning(ex);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Selects the first match, with the highest number of matching terms from a list of parts.
|
|
/// </summary>
|
|
/// <param name="availableParts">The list of parts.</param>
|
|
/// <param name="existingPart">The part to find the closest match for.</param>
|
|
/// <returns>The part with the closest match to the given part.</returns>
|
|
private string FindClosestPartMatch(List<string> availableParts, string existingPart)
|
|
{
|
|
string closestMatch = "None";
|
|
List<string> partSections = existingPart.Split("_").ToList();
|
|
Dictionary<string, int> matchCounts = new Dictionary<string, int>();
|
|
foreach (string section in partSections)
|
|
{
|
|
foreach (string part in availableParts)
|
|
{
|
|
if (part.Contains(section))
|
|
{
|
|
if (matchCounts.TryGetValue(part, out int count))
|
|
{
|
|
matchCounts[part] = count + 1;
|
|
}
|
|
else
|
|
{
|
|
matchCounts[part] = 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
int currentMax = 0;
|
|
|
|
foreach (KeyValuePair<string, int> match in matchCounts)
|
|
{
|
|
if (match.Value > currentMax)
|
|
{
|
|
closestMatch = match.Key;
|
|
currentMax = match.Value;
|
|
}
|
|
}
|
|
|
|
return closestMatch;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generates a character from the current selected parts.
|
|
/// </summary>
|
|
private GameObject GenerateCharacter(bool combineMesh, bool processBoneMovement)
|
|
{
|
|
List<SkinnedMeshRenderer> parts = new List<SkinnedMeshRenderer>();
|
|
foreach (KeyValuePair<CharacterPartType, SidekickPart> entry in _currentCharacter)
|
|
{
|
|
if (entry.Value != null)
|
|
{
|
|
// Only apply wrap when required
|
|
if (entry.Key == CharacterPartType.Wrap && (!_requiresWrap || _bodyTypeBlendValue < 0))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
GameObject partModel = entry.Value.GetPartModel();
|
|
|
|
if (partModel == null)
|
|
{
|
|
if (_showMissingPartsPopup)
|
|
{
|
|
EditorUtility.DisplayDialog(
|
|
"Error loading part",
|
|
"Unable to load part: " + entry.Value.Name + ".",
|
|
"Ok"
|
|
);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
SkinnedMeshRenderer selectedMesh = partModel.GetComponentInChildren<SkinnedMeshRenderer>();
|
|
if (selectedMesh != null)
|
|
{
|
|
parts.Add(selectedMesh);
|
|
}
|
|
}
|
|
}
|
|
|
|
GameObject newModel = null;
|
|
try
|
|
{
|
|
newModel = _sidekickRuntime.CreateCharacter( _OUTPUT_MODEL_NAME, parts, combineMesh, processBoneMovement, _newModel);
|
|
_currentAnimator = null;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
EditorUtility.DisplayDialog(
|
|
"Error creating character",
|
|
"Something went wrong when creating the character.\nPlease try again.",
|
|
"Ok"
|
|
);
|
|
Debug.LogWarning(ex);
|
|
}
|
|
|
|
return newModel;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Saves a character (Parts and Colors) out to a file which can be imported by the tool into any project.
|
|
/// </summary>
|
|
private void SaveCharacter()
|
|
{
|
|
try
|
|
{
|
|
SaveCharacter(null);
|
|
}
|
|
catch
|
|
{
|
|
Debug.LogWarning("Failed to save character. Please try again.");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Saves a character (Parts and Colors) out to a file which can be imported by the tool into any project to a given path.
|
|
/// </summary>
|
|
/// <param name="savePath">The path to save the character to.</param>
|
|
private void SaveCharacter(string savePath)
|
|
{
|
|
bool showSuccessMessage = false;
|
|
|
|
if (string.IsNullOrEmpty(savePath))
|
|
{
|
|
savePath = ShowCharacterSaveDialog();
|
|
showSuccessMessage = true;
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(savePath))
|
|
{
|
|
// EditorUtility.DisplayDialog("Save Cancelled", "No save file selected. Saving cancelled.", "OK");
|
|
return;
|
|
}
|
|
|
|
string filename = Path.GetFileNameWithoutExtension(savePath);
|
|
SerializedCharacter savedCharacter = CreateSerializedCharacter(filename);
|
|
|
|
Serializer serializer = new Serializer();
|
|
|
|
File.WriteAllBytes(savePath, Encoding.ASCII.GetBytes(serializer.Serialize(savedCharacter)));
|
|
if (showSuccessMessage)
|
|
{
|
|
EditorUtility.DisplayDialog("Save Successful", "Character successfully saved to " + Path.GetFileName(savePath), "OK");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Crates a serialized character from the current tool selections.
|
|
/// </summary>
|
|
/// <param name="characterName">The name to store in the serialized character.</param>
|
|
/// <returns>A SerializedCharacter from the selections in the tool.</returns>
|
|
private SerializedCharacter CreateSerializedCharacter(string characterName)
|
|
{
|
|
SerializedCharacter savedCharacter = new SerializedCharacter
|
|
{
|
|
Species = _currentSpecies.ID,
|
|
Name = characterName
|
|
};
|
|
|
|
List<SerializedPart> usedParts = new List<SerializedPart>();
|
|
foreach (KeyValuePair<CharacterPartType, SidekickPart> entry in _currentCharacter)
|
|
{
|
|
// TODO: Update the part version to use actual version once the information is available.
|
|
usedParts.Add(new SerializedPart(entry.Value.Name, entry.Key, "1"));
|
|
}
|
|
|
|
savedCharacter.Parts = usedParts;
|
|
SerializedColorSet savedSet = new SerializedColorSet();
|
|
savedSet.PopulateFromSidekickColorSet(_currentColorSet, _currentSpecies);
|
|
savedCharacter.ColorSet = savedSet;
|
|
|
|
savedCharacter.BlendShapes = new SerializedBlendShapeValues()
|
|
{
|
|
BodyTypeValue = _bodyTypeSlider.value,
|
|
BodySizeValue = _bodySizeSlider.value,
|
|
MuscleValue = _musclesSlider.value
|
|
};
|
|
|
|
List<SerializedColorRow> savedColorRows = new List<SerializedColorRow>();
|
|
foreach (SidekickColorRow row in _allColorRows)
|
|
{
|
|
savedColorRows.Add(new SerializedColorRow(row));
|
|
}
|
|
|
|
savedCharacter.ColorRows = savedColorRows;
|
|
|
|
return savedCharacter;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Loads a character (Parts and Colors) into the tool.
|
|
/// </summary>
|
|
private void LoadCharacter()
|
|
{
|
|
_loadingCharacter = true;
|
|
bool showAllColors = _showAllColourProperties;
|
|
_showAllColourProperties = true;
|
|
|
|
string filePath = EditorUtility.OpenFilePanel("Load Character", "", "sk");
|
|
if (string.IsNullOrEmpty(filePath))
|
|
{
|
|
EditorUtility.DisplayDialog("No File Chosen", "No file was chosen to load.", "OK");
|
|
return;
|
|
}
|
|
|
|
_bodyPartsTab.value = true;
|
|
SwitchToTab(TabView.Parts);
|
|
|
|
byte[] bytes = File.ReadAllBytes(filePath);
|
|
string data = Encoding.ASCII.GetString(bytes);
|
|
|
|
Deserializer deserializer = new Deserializer();
|
|
SerializedCharacter savedCharacter = deserializer.Deserialize<SerializedCharacter>(data);
|
|
|
|
LoadSerializedCharacter(savedCharacter, showAllColors);
|
|
_loadingCharacter = false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Loads a character into the tool from a serialized character.
|
|
/// </summary>
|
|
/// <param name="serializedCharacter">The serialized character to load.</param>
|
|
/// <param name="showAllColors">Whether to show all colors or not.</param>
|
|
private void LoadSerializedCharacter(SerializedCharacter serializedCharacter, bool showAllColors)
|
|
{
|
|
SidekickSpecies species = SidekickSpecies.GetByID(_dbManager, serializedCharacter.Species);
|
|
_speciesField.value = species.Name;
|
|
ProcessSpeciesChange(species.Name);
|
|
|
|
bool hasErrors = false;
|
|
string errorMessage = "The following parts could not be found in your project:\n";
|
|
foreach (CharacterPartType currentType in Enum.GetValues(typeof(CharacterPartType)))
|
|
{
|
|
PartTypeControls currentField = _partSelectionDictionary[currentType];
|
|
SerializedPart part = serializedCharacter.Parts.FirstOrDefault(p => p.PartType == currentType);
|
|
SidekickPart skPart = SidekickPart.SearchForByName(_dbManager, part?.Name);
|
|
if (skPart != null)
|
|
{
|
|
UpdateResult result = UpdatePartDropdown(currentField, skPart.Name, errorMessage, hasErrors);
|
|
hasErrors = result.HasErrors;
|
|
errorMessage = result.ErrorMessage;
|
|
}
|
|
else
|
|
{
|
|
currentField.SetPartDropdownValue("None");
|
|
}
|
|
}
|
|
|
|
if (hasErrors)
|
|
{
|
|
EditorUtility.DisplayDialog(
|
|
"Assets Missing",
|
|
errorMessage,
|
|
"OK"
|
|
);
|
|
}
|
|
|
|
|
|
LoadColorSet(serializedCharacter);
|
|
|
|
if (serializedCharacter.BlendShapes != null)
|
|
{
|
|
LoadBlendShapes(serializedCharacter);
|
|
}
|
|
|
|
_showAllColourProperties = showAllColors;
|
|
UpdateColorTabContent();
|
|
|
|
if (_combineMeshes && _newModel != null)
|
|
{
|
|
DestroyImmediate(_newModel);
|
|
}
|
|
|
|
_newModel = GenerateCharacter(_combineMeshes, true);
|
|
UpdatePartUVData();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates a part dropdown to select a new part, if the part is not in the dropdown values, `None` is selected instead.
|
|
/// </summary>
|
|
/// <param name="currentField">The dropdown field to update.</param>
|
|
/// <param name="partName">The new part to select.</param>
|
|
/// <param name="errorMessage">The error message to update if the part is not available.</param>
|
|
/// <param name="hasErrors">The error flag to update if an error is encountered.</param>
|
|
/// <returns>A PartUpdateResult with the results of the update.</returns>
|
|
private UpdateResult UpdatePartDropdown(PartTypeControls currentField, string partName, string errorMessage, bool hasErrors)
|
|
{
|
|
_partLockMap[currentField.PartDropdown] = false;
|
|
|
|
if (partName == "None" || _allParts.Any(part => part.Name == partName))
|
|
{
|
|
if (!currentField.PartDropdown.choices.Contains(partName) && PartUtils.IsBaseSpeciesPart(partName))
|
|
{
|
|
currentField.SetPartDropdownValue(currentField.PartDropdown.choices.Find(n => n.Contains("BASE")) ?? "None");
|
|
if (currentField.PartDropdown.value == "None")
|
|
{
|
|
hasErrors = true;
|
|
errorMessage += partName + "\n";
|
|
}
|
|
}
|
|
else
|
|
{
|
|
currentField.SetPartDropdownValue(partName);
|
|
}
|
|
}
|
|
else if (PartUtils.IsBaseSpeciesPart(partName))
|
|
{
|
|
currentField.SetPartDropdownValue(currentField.PartDropdown.choices.Find(n => n.Contains("BASE")) ?? "None");
|
|
if (currentField.PartDropdown.value == "None")
|
|
{
|
|
hasErrors = true;
|
|
errorMessage += partName + "\n";
|
|
}
|
|
}
|
|
else
|
|
{
|
|
currentField.SetPartDropdownValue("None");
|
|
hasErrors = true;
|
|
errorMessage += partName + "\n";
|
|
}
|
|
|
|
return new UpdateResult(errorMessage, hasErrors);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Loads the color set for a saved character into memory.
|
|
/// </summary>
|
|
/// <param name="savedCharacter">The character to load the color set for.</param>
|
|
private void LoadColorSet(SerializedCharacter savedCharacter)
|
|
{
|
|
_currentColorSet = savedCharacter.ColorSet.CreateSidekickColorSet(_dbManager);
|
|
_colorSetsDropdown.value = "Custom";
|
|
|
|
List<SidekickColorRow> newRows = new List<SidekickColorRow>();
|
|
foreach (SerializedColorRow row in savedCharacter.ColorRows)
|
|
{
|
|
newRows.Add(row.CreateSidekickColorRow(_dbManager, _currentColorSet));
|
|
}
|
|
|
|
_allColorRows = newRows;
|
|
UpdateColorTabContent();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the content of the Color Tab.
|
|
/// </summary>
|
|
private void UpdateColorTabContent()
|
|
{
|
|
PopulatePartColorRows();
|
|
UpdateAllVisibleColors();
|
|
RefreshVisibleColorRows();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Loads the blend shapes from a saved character into the tool.
|
|
/// </summary>
|
|
/// <param name="savedCharacter">The character to load the blend shapes for.</param>
|
|
private void LoadBlendShapes(SerializedCharacter savedCharacter)
|
|
{
|
|
_bodyTypeSlider.value = savedCharacter.BlendShapes.BodyTypeValue;
|
|
_bodySizeSlider.value = savedCharacter.BlendShapes.BodySizeValue;
|
|
_musclesSlider.value = savedCharacter.BlendShapes.MuscleValue;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Shows the dialog box for where to save the character to, and also validates the save location and filename.
|
|
/// </summary>
|
|
/// <param name="path">The default path to save to.</param>
|
|
/// <returns>The full file path and filename to save the character to.</returns>
|
|
private string ShowCharacterSaveDialog(string path = "")
|
|
{
|
|
string defaultName = _currentSpecies.Name + "-" + _currentColorSet.Name + ".sk";
|
|
string defaultDirectory = "";
|
|
if (!string.IsNullOrEmpty(path))
|
|
{
|
|
defaultName = Path.GetFileName(path);
|
|
defaultDirectory = Path.GetDirectoryName(path);
|
|
}
|
|
|
|
string savePath = EditorUtility.SaveFilePanel(
|
|
"Save New Character",
|
|
defaultDirectory,
|
|
defaultName,
|
|
"sk"
|
|
);
|
|
|
|
// if (!string.IsNullOrEmpty(savePath) && File.Exists(savePath))
|
|
// {
|
|
// int option = EditorUtility.DisplayDialogComplex(
|
|
// "File Already Exists",
|
|
// "A file already exists with the same name, are you sure you wish to overwrite it?\nThis cannot be undone.",
|
|
// "Overwrite",
|
|
// "Rename",
|
|
// "Cancel"
|
|
// );
|
|
//
|
|
// switch (option)
|
|
// {
|
|
// // Overwrite.
|
|
// case 0:
|
|
// EditorUtility.DisplayDialog("Overwrite Accepted", "Existing file will be overwritten.", "OK");
|
|
// break;
|
|
//
|
|
// // Rename.
|
|
// case 1:
|
|
// savePath = ShowCharacterSaveDialog(savePath);
|
|
// break;
|
|
//
|
|
// // Cancel.
|
|
// case 2:
|
|
// default:
|
|
// savePath = null;
|
|
// break;
|
|
// }
|
|
// }
|
|
|
|
return savePath;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Saves a created character as a prefab.
|
|
/// </summary>
|
|
private void CreateCharacterPrefab()
|
|
{
|
|
try
|
|
{
|
|
string savePath = SelectPrefabSaveLocation();
|
|
|
|
if (string.IsNullOrEmpty(savePath))
|
|
{
|
|
// EditorUtility.DisplayDialog("Save Cancelled", "No save file selected. Saving cancelled.", "OK");
|
|
return;
|
|
}
|
|
|
|
string baseFilename = Path.GetFileNameWithoutExtension(savePath);
|
|
string directoryBase = Path.GetDirectoryName(savePath) ?? string.Empty;
|
|
string directory = Path.Combine(directoryBase, baseFilename);
|
|
savePath = Path.Combine(directory, Path.GetFileName(savePath));
|
|
string textureDirectory = Path.Combine(directory, "Textures");
|
|
string meshDirectory = Path.Combine(directory, "Meshes");
|
|
string materialDirectory = Path.Combine(directory, "Materials");
|
|
|
|
if (!Directory.Exists(directory))
|
|
{
|
|
Directory.CreateDirectory(directory);
|
|
}
|
|
|
|
if (!Directory.Exists(meshDirectory))
|
|
{
|
|
Directory.CreateDirectory(meshDirectory);
|
|
}
|
|
|
|
if (!Directory.Exists(materialDirectory))
|
|
{
|
|
Directory.CreateDirectory(materialDirectory);
|
|
}
|
|
|
|
string savedCharacterPath = Path.Combine(directory, baseFilename + ".sk");
|
|
SaveCharacter(savedCharacterPath);
|
|
|
|
//TODO textures are shared between exports!
|
|
SaveTexturesToDisk(textureDirectory, baseFilename);
|
|
// Ensure textures are written to disk before proceeding. As it seems this happens outside the main Unity loop, so can't be easily checked
|
|
// for.
|
|
int cutoff = 0;
|
|
while (cutoff < 1000000000 && Directory.GetFiles(textureDirectory).Length <= 0)
|
|
{
|
|
cutoff++;
|
|
}
|
|
|
|
AssetDatabase.Refresh();
|
|
|
|
GameObject clonedModel = GenerateCharacter(_combineMeshes, false);
|
|
|
|
List<SkinnedMeshRenderer> allRenderers = clonedModel.GetComponentsInChildren<SkinnedMeshRenderer>().ToList();
|
|
SkinnedMeshRenderer clonedRenderer = allRenderers[0];
|
|
|
|
if (_bakeBlends)
|
|
{
|
|
|
|
// Copy mesh, bone weights and bindposes before baking so the mesh can be re-skinned after baking.
|
|
foreach (SkinnedMeshRenderer renderer in allRenderers)
|
|
{
|
|
if (clonedRenderer == null)
|
|
{
|
|
clonedRenderer = renderer;
|
|
}
|
|
|
|
Mesh clonedSkinnedMesh = MeshUtils.CopyMesh(renderer.sharedMesh);
|
|
BoneWeight[] boneWeights = clonedSkinnedMesh.boneWeights;
|
|
Matrix4x4[] bindposes = clonedSkinnedMesh.bindposes;
|
|
List<BlendShapeData> blendData = BlendShapeUtils.GetBlendShapeData(
|
|
clonedSkinnedMesh,
|
|
renderer,
|
|
new string[]
|
|
{
|
|
"defaultHeavy", "defaultBuff", "defaultSkinny", "masculineFeminine"
|
|
},
|
|
0,
|
|
new List<BlendShapeData>()
|
|
);
|
|
renderer.BakeMesh(clonedSkinnedMesh);
|
|
// Re-skin the new baked mesh.
|
|
clonedSkinnedMesh.boneWeights = boneWeights;
|
|
clonedSkinnedMesh.bindposes = bindposes;
|
|
// assign the new mesh to the renderer
|
|
renderer.sharedMesh = clonedSkinnedMesh;
|
|
|
|
BlendShapeUtils.RestoreBlendShapeData(blendData, clonedSkinnedMesh, renderer);
|
|
}
|
|
}
|
|
|
|
// now do the bone movements!
|
|
_sidekickRuntime.ProcessRigMovementOnBlendShapeChange(SidekickBlendShapeRigMovement.GetAllForProcessing(_dbManager));
|
|
_sidekickRuntime.ProcessBoneMovement(clonedModel);
|
|
|
|
Material newMaterial = CreateNewMaterialAssetFromSource(
|
|
clonedRenderer.sharedMaterial,
|
|
textureDirectory,
|
|
materialDirectory,
|
|
baseFilename,
|
|
baseFilename
|
|
);
|
|
|
|
foreach (SkinnedMeshRenderer renderer in allRenderers)
|
|
{
|
|
renderer.sharedMaterial = newMaterial;
|
|
}
|
|
|
|
CreatePrefab(clonedModel, meshDirectory, savePath, baseFilename);
|
|
DestroyImmediate(clonedModel);
|
|
}
|
|
catch
|
|
{
|
|
Debug.LogWarning("Failed to create character prefab, please try again.");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Prompts the user to select a path and prefab name within the project.
|
|
/// </summary>
|
|
/// <returns>The path and filename to use to save the prefab to.</returns>
|
|
private string SelectPrefabSaveLocation()
|
|
{
|
|
string defaultName = _currentSpecies.Name + "-" + _currentColorSet.Name + ".prefab";
|
|
|
|
string savePath = EditorUtility.SaveFilePanelInProject(
|
|
"Save Character Prefab",
|
|
defaultName,
|
|
"prefab",
|
|
"Select where to save the prefab"
|
|
);
|
|
|
|
return savePath;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the material to use the textures at the given location.
|
|
/// </summary>
|
|
/// <param name="material">The material to set the textures on.</param>
|
|
/// <param name="texturePath">The path to set the textures from.</param>
|
|
/// <param name="textureName">Additional naming for the textures, if applicable.</param>
|
|
/// <returns>The material with the paths set on it.</returns>
|
|
private Material SetTextureLinkOnMaterial(Material material, string texturePath, string textureName = "")
|
|
{
|
|
string filename = _TEXTURE_PREFIX;
|
|
|
|
if (!string.IsNullOrEmpty(textureName))
|
|
{
|
|
filename += textureName;
|
|
}
|
|
|
|
material.SetTexture(_COLOR_MAP, null);
|
|
LoadAndAssignTexture(material, texturePath, filename + _TEXTURE_COLOR_NAME, _COLOR_MAP);
|
|
// TODO: Uncomment when the shader has the these properties are enabled again
|
|
// material.SetTexture(_METALLIC_MAP, null);
|
|
// LoadAndAssignTexture(material, texturePath, filename + _TEXTURE_METALLIC_NAME, _METALLIC_MAP);
|
|
// material.SetTexture(_SMOOTHNESS_MAP, null);
|
|
// LoadAndAssignTexture(material, texturePath, filename + _TEXTURE_SMOOTHNESS_NAME, _SMOOTHNESS_MAP);
|
|
// material.SetTexture(_REFLECTION_MAP, null);
|
|
// LoadAndAssignTexture(material, texturePath, filename + _TEXTURE_REFLECTION_NAME, _REFLECTION_MAP);
|
|
// material.SetTexture(_EMISSION_MAP, null);
|
|
// LoadAndAssignTexture(material, texturePath, filename + _TEXTURE_EMISSION_NAME, _EMISSION_MAP);
|
|
// material.SetTexture(_OPACITY_MAP, null);
|
|
// LoadAndAssignTexture(material, texturePath, filename + _TEXTURE_OPACITY_NAME, _OPACITY_MAP);
|
|
|
|
return material;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Loads a texture from disk and assigns it to the material in the given texture ID.
|
|
/// </summary>
|
|
/// <param name="material">The material to assign the texture to.</param>
|
|
/// <param name="texturePath">The path to load the texture from.</param>
|
|
/// <param name="textureName">The name of the texture to load.</param>
|
|
/// <param name="textureID">The texture ID to load the texture into on the material.</param>
|
|
private void LoadAndAssignTexture(Material material, string texturePath, string textureName, int textureID)
|
|
{
|
|
string filePath = Path.Combine(texturePath, textureName);
|
|
while (material.GetTexture(textureID) == null)
|
|
{
|
|
TextureImporter textureImporter = AssetImporter.GetAtPath(filePath) as TextureImporter;
|
|
if (textureImporter != null)
|
|
{
|
|
textureImporter.wrapMode = TextureWrapMode.Clamp;
|
|
textureImporter.filterMode = FilterMode.Point;
|
|
textureImporter.mipmapEnabled = false;
|
|
textureImporter.SetPlatformTextureSettings(new TextureImporterPlatformSettings
|
|
{
|
|
maxTextureSize = 32,
|
|
resizeAlgorithm = TextureResizeAlgorithm.Bilinear,
|
|
format = TextureImporterFormat.RGB24
|
|
});
|
|
EditorUtility.SetDirty(textureImporter);
|
|
textureImporter.SaveAndReimport();
|
|
}
|
|
|
|
material.SetTexture(textureID, (Texture2D) AssetDatabase.LoadAssetAtPath(filePath, typeof(Texture2D)));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new material to assign to the prefab.
|
|
/// </summary>
|
|
/// <param name="sourceMaterial">The existing material from the base model.</param>
|
|
/// <param name="textureDirectory">The directory to save the textures into.</param>
|
|
/// <param name="materialDirectory">The directory to save the material into.</param>
|
|
/// <param name="baseFilename">The base filename to use for all assets.</param>
|
|
/// <param name="textureName">Additional naming for the textures, if applicable.</param>
|
|
/// <returns>The new material cloned from sourceMaterial saved to the asset database.</returns>
|
|
private Material CreateNewMaterialAssetFromSource(
|
|
Material sourceMaterial,
|
|
string textureDirectory,
|
|
string materialDirectory,
|
|
string baseFilename,
|
|
string textureName = ""
|
|
)
|
|
{
|
|
Material clonedMaterial = new Material(sourceMaterial.shader);
|
|
// NOTE: this is copying the texture slots from oldMaterial, so they need to be null'd afterward in SetTextureLinkOnMaterial()
|
|
clonedMaterial.CopyPropertiesFromMaterial(sourceMaterial);
|
|
clonedMaterial = SetTextureLinkOnMaterial(clonedMaterial, textureDirectory, textureName);
|
|
string materialPath = Path.Combine(materialDirectory, baseFilename + ".mat");
|
|
// If the user has chosen to overwrite the prefab, delete the existing assets to replace them.
|
|
if (File.Exists(materialPath))
|
|
{
|
|
File.Delete(materialPath);
|
|
}
|
|
|
|
AssetDatabase.CreateAsset(clonedMaterial, materialPath);
|
|
return clonedMaterial;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a prefab and the required assets for the model to work as an independent asset.
|
|
/// </summary>
|
|
/// <param name="rootGameObject">Root game object for the prefab.</param>
|
|
/// <param name="meshDirectory">The directory to save the mesh and avatar assets to.</param>
|
|
/// <param name="savePath">The path to save the prefab to.</param>
|
|
/// <param name="baseFilename">The base filename to use for all assets.</param>
|
|
private void CreatePrefab(
|
|
GameObject rootGameObject,
|
|
string meshDirectory,
|
|
string savePath,
|
|
string baseFilename
|
|
)
|
|
{
|
|
List<SkinnedMeshRenderer> renderers = rootGameObject.GetComponentsInChildren<SkinnedMeshRenderer>().ToList();
|
|
foreach (SkinnedMeshRenderer renderer in renderers)
|
|
{
|
|
string type = null;
|
|
if (renderer.name.Contains("_"))
|
|
{
|
|
type = Enum.GetName(typeof(CharacterPartType), _sidekickRuntime.ExtractPartType(renderer.name));
|
|
}
|
|
Mesh sharedMesh = renderer.sharedMesh;
|
|
string meshPath = type == null ? Path.Combine(meshDirectory, baseFilename + ".asset") : Path.Combine(meshDirectory, baseFilename + "-" + type + ".asset");
|
|
// If the user has chosen to overwrite the prefab, delete the existing assets to replace them.
|
|
if (File.Exists(meshPath))
|
|
{
|
|
File.Delete(meshPath);
|
|
}
|
|
|
|
AssetDatabase.CreateAsset(sharedMesh, meshPath);
|
|
}
|
|
|
|
Animator animator = rootGameObject.GetComponentInChildren<Animator>();
|
|
Avatar existingAvatar = animator.avatar;
|
|
Avatar newAvatar = Instantiate(existingAvatar);
|
|
animator.avatar = newAvatar;
|
|
string avatarPath = Path.Combine(meshDirectory, baseFilename + "-avatar.asset");
|
|
// If the user has chosen to overwrite the prefab, delete the existing assets to replace them.
|
|
if (File.Exists(avatarPath))
|
|
{
|
|
File.Delete(avatarPath);
|
|
}
|
|
|
|
AssetDatabase.CreateAsset(newAvatar, avatarPath);
|
|
AssetDatabase.SaveAssets();
|
|
PrefabUtility.SaveAsPrefabAsset(rootGameObject, savePath);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the "outfit" name from the part name.
|
|
/// TODO: This will be replaced once parts and outfits have a proper relationship.
|
|
/// </summary>
|
|
/// <param name="partName">The part name to parse the "outfit" name from.</param>
|
|
/// <returns>The "outfit" name.</returns>
|
|
private string GetOutfitNameFromPartName(string partName)
|
|
{
|
|
if (string.IsNullOrEmpty(partName))
|
|
{
|
|
return "None";
|
|
}
|
|
|
|
return string.Join('_', partName.Substring(3).Split('_').Take(2));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the part UV data.
|
|
/// </summary>
|
|
private void UpdatePartUVData()
|
|
{
|
|
_currentUVDictionary = _sidekickRuntime.CurrentUVDictionary;
|
|
_currentUVList = _sidekickRuntime.CurrentUVList;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The available tab views
|
|
/// </summary>
|
|
private enum TabView
|
|
{
|
|
Preset,
|
|
Parts,
|
|
Body,
|
|
Colors,
|
|
Decals,
|
|
Options
|
|
}
|
|
|
|
/// <summary>
|
|
/// The different types of preset dropdown
|
|
/// </summary>
|
|
private enum PresetDropdownType
|
|
{
|
|
Part,
|
|
Body,
|
|
Color,
|
|
Texture
|
|
}
|
|
|
|
/// <summary>
|
|
/// Encapsulates the result from a dropdown update attempt.
|
|
/// </summary>
|
|
private class UpdateResult
|
|
{
|
|
public string ErrorMessage { get; private set; }
|
|
public bool HasErrors { get; private set; }
|
|
|
|
public UpdateResult(string errorMessage, bool hasErrors)
|
|
{
|
|
ErrorMessage = errorMessage;
|
|
HasErrors = hasErrors;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endif
|