AnimationWindow.cs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694
  1. //********************************** Banshee Engine (www.banshee3d.com) **************************************************//
  2. //**************** Copyright (c) 2016 Marko Pintera ([email protected]). All rights reserved. **********************//
  3. using System;
  4. using System.Collections.Generic;
  5. using BansheeEngine;
  6. namespace BansheeEditor
  7. {
  8. /** @addtogroup Windows
  9. * @{
  10. */
  11. /// <summary>
  12. /// Displays animation curve editor window.
  13. /// </summary>
  14. [DefaultSize(900, 500)]
  15. internal class AnimationWindow : EditorWindow
  16. {
  17. /// <summary>
  18. /// A set of animation curves for a field of a certain type.
  19. /// </summary>
  20. private struct FieldCurves
  21. {
  22. public SerializableProperty.FieldType type;
  23. public EdAnimationCurve[] curves;
  24. }
  25. private const int FIELD_DISPLAY_WIDTH = 200;
  26. private bool isInitialized;
  27. private GUIButton playButton;
  28. private GUIButton recordButton;
  29. private GUIButton prevFrameButton;
  30. private GUIIntField frameInputField;
  31. private GUIButton nextFrameButton;
  32. private GUIButton addKeyframeButton;
  33. private GUIButton addEventButton;
  34. private GUIButton optionsButton;
  35. private GUIButton addPropertyBtn;
  36. private GUIButton delPropertyBtn;
  37. private GUILayout buttonLayout;
  38. private int buttonLayoutHeight;
  39. private GUIPanel editorPanel;
  40. private GUIAnimFieldDisplay guiFieldDisplay;
  41. private GUICurveEditor guiCurveEditor;
  42. private SceneObject selectedSO;
  43. private int currentFrameIdx;
  44. private int fps = 1;
  45. private Dictionary<string, FieldCurves> curves = new Dictionary<string, FieldCurves>();
  46. private List<string> selectedFields = new List<string>();
  47. internal int FPS
  48. {
  49. get { return fps; }
  50. set { guiCurveEditor.SetFPS(value); fps = MathEx.Max(value, 1); }
  51. }
  52. /// <summary>
  53. /// Opens the animation window.
  54. /// </summary>
  55. [MenuItem("Windows/Animation", ButtonModifier.CtrlAlt, ButtonCode.A, 6000)]
  56. private static void OpenGameWindow()
  57. {
  58. OpenWindow<AnimationWindow>();
  59. }
  60. /// <inheritdoc/>
  61. protected override LocString GetDisplayName()
  62. {
  63. return new LocEdString("Animation");
  64. }
  65. private void OnInitialize()
  66. {
  67. Selection.OnSelectionChanged += OnSelectionChanged;
  68. EditorInput.OnPointerPressed += OnPointerPressed;
  69. EditorInput.OnPointerMoved += OnPointerMoved;
  70. EditorInput.OnPointerReleased += OnPointerReleased;
  71. EditorInput.OnButtonUp += OnButtonUp;
  72. Rebuild();
  73. }
  74. private void OnEditorUpdate()
  75. {
  76. }
  77. private void OnDestroy()
  78. {
  79. Selection.OnSelectionChanged -= OnSelectionChanged;
  80. EditorInput.OnPointerPressed -= OnPointerPressed;
  81. EditorInput.OnPointerMoved -= OnPointerMoved;
  82. EditorInput.OnPointerReleased -= OnPointerReleased;
  83. EditorInput.OnButtonUp -= OnButtonUp;
  84. }
  85. protected override void WindowResized(int width, int height)
  86. {
  87. if (!isInitialized)
  88. return;
  89. guiFieldDisplay.SetSize(FIELD_DISPLAY_WIDTH, height - buttonLayoutHeight*2);
  90. int curveEditorWidth = Math.Max(0, width - FIELD_DISPLAY_WIDTH);
  91. guiCurveEditor.SetSize(curveEditorWidth, height - buttonLayoutHeight);
  92. guiCurveEditor.Redraw();
  93. }
  94. private void Rebuild()
  95. {
  96. GUI.Clear();
  97. selectedFields.Clear();
  98. curves.Clear();
  99. isInitialized = false;
  100. selectedSO = Selection.SceneObject;
  101. if (selectedSO == null)
  102. {
  103. GUILabel warningLbl = new GUILabel(new LocEdString("Select an object to animate in the Hierarchy or Scene windows."));
  104. GUILayoutY vertLayout = GUI.AddLayoutY();
  105. vertLayout.AddFlexibleSpace();
  106. GUILayoutX horzLayout = vertLayout.AddLayoutX();
  107. vertLayout.AddFlexibleSpace();
  108. horzLayout.AddFlexibleSpace();
  109. horzLayout.AddElement(warningLbl);
  110. horzLayout.AddFlexibleSpace();
  111. return;
  112. }
  113. // TODO - Retrieve Animation & AnimationClip from the selected object, fill curves dictionary
  114. // - If not available, show a button to create new animation clip
  115. // Top button row
  116. GUIContent playIcon = new GUIContent(EditorBuiltin.GetAnimationWindowIcon(AnimationWindowIcon.Play),
  117. new LocEdString("Play"));
  118. GUIContent recordIcon = new GUIContent(EditorBuiltin.GetAnimationWindowIcon(AnimationWindowIcon.Record),
  119. new LocEdString("Record"));
  120. GUIContent prevFrameIcon = new GUIContent(EditorBuiltin.GetAnimationWindowIcon(AnimationWindowIcon.FrameBack),
  121. new LocEdString("Previous frame"));
  122. GUIContent nextFrameIcon = new GUIContent(EditorBuiltin.GetAnimationWindowIcon(AnimationWindowIcon.FrameForward),
  123. new LocEdString("Next frame"));
  124. GUIContent addKeyframeIcon = new GUIContent(EditorBuiltin.GetAnimationWindowIcon(AnimationWindowIcon.AddKeyframe),
  125. new LocEdString("Add keyframe"));
  126. GUIContent addEventIcon = new GUIContent(EditorBuiltin.GetAnimationWindowIcon(AnimationWindowIcon.AddEvent),
  127. new LocEdString("Add event"));
  128. GUIContent optionsIcon = new GUIContent(EditorBuiltin.GetLibraryWindowIcon(LibraryWindowIcon.Options),
  129. new LocEdString("Options"));
  130. playButton = new GUIButton(playIcon);
  131. recordButton = new GUIButton(recordIcon);
  132. prevFrameButton = new GUIButton(prevFrameIcon);
  133. frameInputField = new GUIIntField();
  134. nextFrameButton = new GUIButton(nextFrameIcon);
  135. addKeyframeButton = new GUIButton(addKeyframeIcon);
  136. addEventButton = new GUIButton(addEventIcon);
  137. optionsButton = new GUIButton(optionsIcon);
  138. playButton.OnClick += () =>
  139. {
  140. // TODO
  141. // - Record current state of the scene object hierarchy
  142. // - Evaluate all curves manually and update them
  143. // - On end, restore original values of the scene object hierarchy
  144. };
  145. recordButton.OnClick += () =>
  146. {
  147. // TODO
  148. // - Every frame read back current values of all the current curve's properties and assign it to the current frame
  149. };
  150. prevFrameButton.OnClick += () =>
  151. {
  152. SetCurrentFrame(currentFrameIdx - 1);
  153. };
  154. frameInputField.OnChanged += SetCurrentFrame;
  155. nextFrameButton.OnClick += () =>
  156. {
  157. SetCurrentFrame(currentFrameIdx + 1);
  158. };
  159. addKeyframeButton.OnClick += () =>
  160. {
  161. guiCurveEditor.AddKeyFrameAtMarker();
  162. // TODO - Update local curves?
  163. };
  164. addEventButton.OnClick += () =>
  165. {
  166. // TODO - Add event
  167. };
  168. optionsButton.OnClick += () =>
  169. {
  170. Vector2I openPosition = ScreenToWindowPos(Input.PointerPosition);
  171. AnimationOptions dropDown = DropDownWindow.Open<AnimationOptions>(this, openPosition);
  172. dropDown.Initialize(this);
  173. };
  174. // Property buttons
  175. addPropertyBtn = new GUIButton(new LocEdString("Add property"));
  176. delPropertyBtn = new GUIButton(new LocEdString("Delete selected"));
  177. addPropertyBtn.OnClick += () =>
  178. {
  179. Vector2I windowPos = ScreenToWindowPos(Input.PointerPosition);
  180. FieldSelectionWindow fieldSelection = DropDownWindow.Open<FieldSelectionWindow>(this, windowPos);
  181. fieldSelection.OnFieldSelected += OnFieldAdded;
  182. };
  183. delPropertyBtn.OnClick += () =>
  184. {
  185. LocEdString title = new LocEdString("Warning");
  186. LocEdString message = new LocEdString("Are you sure you want to remove all selected fields?");
  187. DialogBox.Open(title, message, DialogBox.Type.YesNo, x =>
  188. {
  189. if (x == DialogBox.ResultType.Yes)
  190. {
  191. RemoveSelectedFields();
  192. }
  193. });
  194. };
  195. GUILayout mainLayout = GUI.AddLayoutY();
  196. buttonLayout = mainLayout.AddLayoutX();
  197. buttonLayout.AddSpace(5);
  198. buttonLayout.AddElement(playButton);
  199. buttonLayout.AddElement(recordButton);
  200. buttonLayout.AddSpace(5);
  201. buttonLayout.AddElement(prevFrameButton);
  202. buttonLayout.AddElement(frameInputField);
  203. buttonLayout.AddElement(nextFrameButton);
  204. buttonLayout.AddSpace(5);
  205. buttonLayout.AddElement(addKeyframeButton);
  206. buttonLayout.AddElement(addEventButton);
  207. buttonLayout.AddSpace(5);
  208. buttonLayout.AddElement(optionsButton);
  209. buttonLayout.AddFlexibleSpace();
  210. buttonLayoutHeight = playButton.Bounds.height;
  211. GUILayout contentLayout = mainLayout.AddLayoutX();
  212. GUILayout fieldDisplayLayout = contentLayout.AddLayoutY(GUIOption.FixedWidth(FIELD_DISPLAY_WIDTH));
  213. guiFieldDisplay = new GUIAnimFieldDisplay(fieldDisplayLayout, FIELD_DISPLAY_WIDTH,
  214. Height - buttonLayoutHeight * 2, selectedSO);
  215. guiFieldDisplay.OnEntrySelected += OnFieldSelected;
  216. GUILayout bottomButtonLayout = fieldDisplayLayout.AddLayoutX();
  217. bottomButtonLayout.AddElement(addPropertyBtn);
  218. bottomButtonLayout.AddElement(delPropertyBtn);
  219. GUILayout curveLayout = contentLayout.AddLayoutY();
  220. editorPanel = curveLayout.AddPanel();
  221. int curveEditorWidth = Math.Max(0, Width - FIELD_DISPLAY_WIDTH);
  222. guiCurveEditor = new GUICurveEditor(this, editorPanel, curveEditorWidth, Height - buttonLayoutHeight);
  223. guiCurveEditor.OnFrameSelected += OnFrameSelected;
  224. guiCurveEditor.Redraw();
  225. SetCurrentFrame(currentFrameIdx);
  226. isInitialized = true;
  227. }
  228. private void SetCurrentFrame(int frameIdx)
  229. {
  230. currentFrameIdx = Math.Max(0, frameIdx);
  231. frameInputField.Value = currentFrameIdx;
  232. guiCurveEditor.SetMarkedFrame(currentFrameIdx);
  233. float time = guiCurveEditor.GetTimeForFrame(currentFrameIdx);
  234. List<GUIAnimFieldPathValue> values = new List<GUIAnimFieldPathValue>();
  235. foreach (var kvp in curves)
  236. {
  237. SerializableProperty property = GUIAnimFieldDisplay.FindProperty(selectedSO, kvp.Key);
  238. if (property != null)
  239. {
  240. GUIAnimFieldPathValue fieldValue = new GUIAnimFieldPathValue();
  241. fieldValue.path = kvp.Key;
  242. switch (kvp.Value.type)
  243. {
  244. case SerializableProperty.FieldType.Vector2:
  245. {
  246. Vector2 value = new Vector2();
  247. for(int i = 0; i < 2; i++)
  248. value[i] = kvp.Value.curves[i].Evaluate(time, false);
  249. fieldValue.value = value;
  250. }
  251. break;
  252. case SerializableProperty.FieldType.Vector3:
  253. {
  254. Vector3 value = new Vector3();
  255. for (int i = 0; i < 3; i++)
  256. value[i] = kvp.Value.curves[i].Evaluate(time, false);
  257. fieldValue.value = value;
  258. }
  259. break;
  260. case SerializableProperty.FieldType.Vector4:
  261. {
  262. Vector4 value = new Vector4();
  263. for (int i = 0; i < 4; i++)
  264. value[i] = kvp.Value.curves[i].Evaluate(time, false);
  265. fieldValue.value = value;
  266. }
  267. break;
  268. case SerializableProperty.FieldType.Color:
  269. {
  270. Color value = new Color();
  271. for (int i = 0; i < 4; i++)
  272. value[i] = kvp.Value.curves[i].Evaluate(time, false);
  273. fieldValue.value = value;
  274. }
  275. break;
  276. case SerializableProperty.FieldType.Bool:
  277. case SerializableProperty.FieldType.Int:
  278. case SerializableProperty.FieldType.Float:
  279. fieldValue.value = kvp.Value.curves[0].Evaluate(time, false); ;
  280. break;
  281. }
  282. values.Add(fieldValue);
  283. }
  284. }
  285. guiFieldDisplay.SetDisplayValues(values.ToArray());
  286. }
  287. private void OnPointerPressed(PointerEvent ev)
  288. {
  289. if (!isInitialized)
  290. return;
  291. guiCurveEditor.OnPointerPressed(ev);
  292. }
  293. private void OnPointerMoved(PointerEvent ev)
  294. {
  295. if (!isInitialized)
  296. return;
  297. guiCurveEditor.OnPointerMoved(ev);
  298. }
  299. private void OnPointerReleased(PointerEvent ev)
  300. {
  301. if (!isInitialized)
  302. return;
  303. guiCurveEditor.OnPointerReleased(ev);
  304. }
  305. private void OnButtonUp(ButtonEvent ev)
  306. {
  307. if (!isInitialized)
  308. return;
  309. guiCurveEditor.OnButtonUp(ev);
  310. }
  311. private void UpdateDisplayedCurves()
  312. {
  313. List<EdAnimationCurve> curvesToDisplay = new List<EdAnimationCurve>();
  314. for (int i = 0; i < selectedFields.Count; i++)
  315. {
  316. EdAnimationCurve curve;
  317. if(TryGetCurve(selectedFields[i], out curve))
  318. curvesToDisplay.Add(curve);
  319. }
  320. guiCurveEditor.SetCurves(curvesToDisplay.ToArray());
  321. float xRange;
  322. float yRange;
  323. CalculateRange(curvesToDisplay, out xRange, out yRange);
  324. // Don't allow zero range
  325. if (xRange == 0.0f)
  326. xRange = 60.0f;
  327. if (yRange == 0.0f)
  328. yRange = 10.0f;
  329. // Add padding to y range
  330. yRange *= 1.05f;
  331. // Don't reduce visible range
  332. xRange = Math.Max(xRange, guiCurveEditor.XRange);
  333. yRange = Math.Max(yRange, guiCurveEditor.YRange);
  334. guiCurveEditor.SetRange(xRange, yRange);
  335. guiCurveEditor.Redraw();
  336. }
  337. private static void CalculateRange(List<EdAnimationCurve> curves, out float xRange, out float yRange)
  338. {
  339. xRange = 0.0f;
  340. yRange = 0.0f;
  341. foreach (var curve in curves)
  342. {
  343. KeyFrame[] keyframes = curve.KeyFrames;
  344. foreach (var key in keyframes)
  345. {
  346. xRange = Math.Max(xRange, key.time);
  347. yRange = Math.Max(yRange, Math.Abs(key.value));
  348. }
  349. }
  350. }
  351. private bool TryGetCurve(string path, out EdAnimationCurve curve)
  352. {
  353. int index = path.LastIndexOf(".");
  354. string parentPath;
  355. string subPathSuffix = null;
  356. if (index == -1)
  357. {
  358. parentPath = path;
  359. }
  360. else
  361. {
  362. parentPath = path.Substring(0, index);
  363. subPathSuffix = path.Substring(index, path.Length - index);
  364. }
  365. FieldCurves fieldCurves;
  366. if (curves.TryGetValue(parentPath, out fieldCurves))
  367. {
  368. if (!string.IsNullOrEmpty(subPathSuffix))
  369. {
  370. if (subPathSuffix == ".x" || subPathSuffix == ".r")
  371. {
  372. curve = fieldCurves.curves[0];
  373. return true;
  374. }
  375. else if (subPathSuffix == ".y" || subPathSuffix == ".g")
  376. {
  377. curve = fieldCurves.curves[1];
  378. return true;
  379. }
  380. else if (subPathSuffix == ".z" || subPathSuffix == ".b")
  381. {
  382. curve = fieldCurves.curves[2];
  383. return true;
  384. }
  385. else if (subPathSuffix == ".w" || subPathSuffix == ".a")
  386. {
  387. curve = fieldCurves.curves[3];
  388. return true;
  389. }
  390. }
  391. else
  392. {
  393. curve = fieldCurves.curves[0];
  394. return true;
  395. }
  396. }
  397. curve = null;
  398. return false;
  399. }
  400. private void OnFieldAdded(string path, SerializableProperty.FieldType type)
  401. {
  402. guiFieldDisplay.AddField(path);
  403. switch (type)
  404. {
  405. case SerializableProperty.FieldType.Vector4:
  406. {
  407. FieldCurves fieldCurves = new FieldCurves();
  408. fieldCurves.type = type;
  409. fieldCurves.curves = new EdAnimationCurve[4];
  410. string[] subPaths = { ".x", ".y", ".z", ".w" };
  411. for (int i = 0; i < subPaths.Length; i++)
  412. {
  413. string subFieldPath = path + subPaths[i];
  414. fieldCurves.curves[i] = new EdAnimationCurve();
  415. selectedFields.Add(subFieldPath);
  416. }
  417. curves[path] = fieldCurves;
  418. }
  419. break;
  420. case SerializableProperty.FieldType.Vector3:
  421. {
  422. FieldCurves fieldCurves = new FieldCurves();
  423. fieldCurves.type = type;
  424. fieldCurves.curves = new EdAnimationCurve[3];
  425. string[] subPaths = { ".x", ".y", ".z" };
  426. for (int i = 0; i < subPaths.Length; i++)
  427. {
  428. string subFieldPath = path + subPaths[i];
  429. fieldCurves.curves[i] = new EdAnimationCurve();
  430. selectedFields.Add(subFieldPath);
  431. }
  432. curves[path] = fieldCurves;
  433. }
  434. break;
  435. case SerializableProperty.FieldType.Vector2:
  436. {
  437. FieldCurves fieldCurves = new FieldCurves();
  438. fieldCurves.type = type;
  439. fieldCurves.curves = new EdAnimationCurve[2];
  440. string[] subPaths = { ".x", ".y" };
  441. for (int i = 0; i < subPaths.Length; i++)
  442. {
  443. string subFieldPath = path + subPaths[i];
  444. fieldCurves.curves[i] = new EdAnimationCurve();
  445. selectedFields.Add(subFieldPath);
  446. }
  447. curves[path] = fieldCurves;
  448. }
  449. break;
  450. case SerializableProperty.FieldType.Color:
  451. {
  452. FieldCurves fieldCurves = new FieldCurves();
  453. fieldCurves.type = type;
  454. fieldCurves.curves = new EdAnimationCurve[4];
  455. string[] subPaths = { ".r", ".g", ".b", ".a" };
  456. for (int i = 0; i < subPaths.Length; i++)
  457. {
  458. string subFieldPath = path + subPaths[i];
  459. fieldCurves.curves[i] = new EdAnimationCurve();
  460. selectedFields.Add(subFieldPath);
  461. }
  462. curves[path] = fieldCurves;
  463. }
  464. break;
  465. default: // Primitive type
  466. {
  467. FieldCurves fieldCurves = new FieldCurves();
  468. fieldCurves.type = type;
  469. fieldCurves.curves = new EdAnimationCurve[1];
  470. fieldCurves.curves[0] = new EdAnimationCurve();
  471. selectedFields.Add(path);
  472. curves[path] = fieldCurves;
  473. }
  474. break;
  475. }
  476. UpdateDisplayedCurves();
  477. }
  478. private bool IsPathParent(string child, string parent)
  479. {
  480. string[] childEntries = child.Split('/', '.');
  481. string[] parentEntries = parent.Split('/', '.');
  482. if (parentEntries.Length >= child.Length)
  483. return false;
  484. int compareLength = Math.Min(childEntries.Length, parentEntries.Length);
  485. for (int i = 0; i < compareLength; i++)
  486. {
  487. if (childEntries[i] != parentEntries[i])
  488. return false;
  489. }
  490. return true;
  491. }
  492. private string GetSubPathParent(string path)
  493. {
  494. int index = path.LastIndexOf(".");
  495. if (index == -1)
  496. return path;
  497. return path.Substring(0, index);
  498. }
  499. private void OnFieldSelected(string path)
  500. {
  501. if (!Input.IsButtonHeld(ButtonCode.LeftShift) && !Input.IsButtonHeld(ButtonCode.RightShift))
  502. selectedFields.Clear();
  503. if (!string.IsNullOrEmpty(path))
  504. {
  505. selectedFields.RemoveAll(x => { return x == path || IsPathParent(x, path); });
  506. selectedFields.Add(path);
  507. }
  508. guiFieldDisplay.SetSelection(selectedFields.ToArray());
  509. UpdateDisplayedCurves();
  510. }
  511. private void RemoveSelectedFields()
  512. {
  513. for (int i = 0; i < selectedFields.Count; i++)
  514. {
  515. selectedFields.Remove(selectedFields[i]);
  516. curves.Remove(GetSubPathParent(selectedFields[i]));
  517. }
  518. List<string> existingFields = new List<string>();
  519. foreach(var KVP in curves)
  520. existingFields.Add(KVP.Key);
  521. guiFieldDisplay.SetFields(existingFields.ToArray());
  522. selectedFields.Clear();
  523. UpdateDisplayedCurves();
  524. }
  525. private void OnSelectionChanged(SceneObject[] sceneObjects, string[] resourcePaths)
  526. {
  527. Rebuild();
  528. }
  529. private void OnFrameSelected(int frameIdx)
  530. {
  531. SetCurrentFrame(frameIdx);
  532. }
  533. }
  534. /// <summary>
  535. /// Drop down window that displays options used by the animation window.
  536. /// </summary>
  537. [DefaultSize(100, 50)]
  538. internal class AnimationOptions : DropDownWindow
  539. {
  540. private AnimationWindow parent;
  541. /// <summary>
  542. /// Initializes the drop down window by creating the necessary GUI. Must be called after construction and before
  543. /// use.
  544. /// </summary>
  545. /// <param name="parent">Animation window that this drop down window is a part of.</param>
  546. internal void Initialize(AnimationWindow parent)
  547. {
  548. this.parent = parent;
  549. GUIIntField fpsField = new GUIIntField(new LocEdString("FPS"), 40);
  550. fpsField.Value = parent.FPS;
  551. fpsField.OnChanged += x => { parent.FPS = x; };
  552. GUILayoutY vertLayout = GUI.AddLayoutY();
  553. vertLayout.AddFlexibleSpace();
  554. GUILayoutX contentLayout = vertLayout.AddLayoutX();
  555. contentLayout.AddFlexibleSpace();
  556. contentLayout.AddElement(fpsField);
  557. contentLayout.AddFlexibleSpace();
  558. vertLayout.AddFlexibleSpace();
  559. }
  560. }
  561. /** @} */
  562. }