Add prototype gameplay loop with character/battlefield assets and server game APIs.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -16,6 +16,8 @@ namespace AOT.Boot
|
||||
public string WsUrlTemplate;
|
||||
/// <summary>CDN version.json 下发的维护/公告文案(可空)。</summary>
|
||||
public string Notice;
|
||||
/// <summary>业务 UI 当前仅使用 candy;字段保留用于旧启动配置兼容。</summary>
|
||||
public string UiTheme = "candy";
|
||||
|
||||
public string BuildWsUrl(string pid, string token = null)
|
||||
{
|
||||
|
||||
@@ -30,6 +30,9 @@ namespace AOT.Boot
|
||||
[Tooltip("WebSocket 模板,{pid}/{token} 会被替换")]
|
||||
public string WsUrlTemplate = "wss://game.xworld.ren/ws?pid={pid}&token={token}";
|
||||
|
||||
[Tooltip("业务 UI 当前仅支持 candy(糖果风);保留该字段兼容旧配置,其他值回退到 Candy。")]
|
||||
public string UiTheme = "candy";
|
||||
|
||||
[Tooltip("运行模式;编辑器默认 EditorSimulateMode;M1 Offline/Host 验证时切换")]
|
||||
public EPlayMode PlayMode = EPlayMode.EditorSimulateMode;
|
||||
|
||||
|
||||
@@ -77,6 +77,7 @@ namespace AOT.Patch
|
||||
HttpBase = _settings.HttpBase,
|
||||
WsUrlTemplate = _settings.WsUrlTemplate,
|
||||
Notice = version.notice ?? string.Empty,
|
||||
UiTheme = string.IsNullOrWhiteSpace(version.uiTheme) ? _settings.UiTheme : version.uiTheme,
|
||||
};
|
||||
|
||||
#if UNITY_EDITOR
|
||||
@@ -152,6 +153,61 @@ namespace AOT.Patch
|
||||
}
|
||||
}
|
||||
|
||||
// UI Prefabs are AssetBundles in Art; DefaultPackage is the raw hot-update DLL.
|
||||
// Initialize explicitly listed companion packages before gameplay starts.
|
||||
var prepared = new System.Collections.Generic.HashSet<string>(StringComparer.Ordinal) { _settings.PackageName };
|
||||
foreach (var packageName in version.packages ?? Array.Empty<string>())
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(packageName) || !prepared.Add(packageName)) continue;
|
||||
if (!YooAssets.TryGetPackage(packageName, out var companion))
|
||||
companion = YooAssets.CreatePackage(packageName);
|
||||
var companionRoot = cdnRoot.TrimEnd('/');
|
||||
var slash = companionRoot.LastIndexOf('/');
|
||||
companionRoot = companionRoot.Substring(0, slash + 1) + packageName;
|
||||
_view?.SetStatus($"初始化资源包 {packageName}…");
|
||||
if (companion.InitializeStatus != EOperationStatus.Succeeded)
|
||||
{
|
||||
var companionInit = CreateInitializeOperation(companion, playMode, companionRoot);
|
||||
if (companionInit == null)
|
||||
{
|
||||
onDone?.Invoke(null, $"不支持资源包 {packageName} 的 PlayMode: {playMode}");
|
||||
yield break;
|
||||
}
|
||||
yield return companionInit;
|
||||
if (companionInit.Status != EOperationStatus.Succeeded)
|
||||
{
|
||||
onDone?.Invoke(null, $"资源包 {packageName} 初始化失败: {companionInit.Error}");
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
var companionVersion = companion.RequestPackageVersionAsync();
|
||||
yield return companionVersion;
|
||||
if (companionVersion.Status != EOperationStatus.Succeeded)
|
||||
{
|
||||
onDone?.Invoke(null, $"资源包 {packageName} 请求版本失败: {companionVersion.Error}");
|
||||
yield break;
|
||||
}
|
||||
var companionManifest = companion.LoadPackageManifestAsync(new LoadPackageManifestOptions(companionVersion.PackageVersion, 60));
|
||||
yield return companionManifest;
|
||||
if (companionManifest.Status != EOperationStatus.Succeeded)
|
||||
{
|
||||
onDone?.Invoke(null, $"资源包 {packageName} 更新清单失败: {companionManifest.Error}");
|
||||
yield break;
|
||||
}
|
||||
var companionDownload = companion.CreateResourceDownloader(new ResourceDownloaderOptions(10, 3));
|
||||
if (companionDownload.TotalDownloadCount > 0)
|
||||
{
|
||||
_view?.SetStatus($"下载资源包 {packageName}…");
|
||||
companionDownload.StartDownload();
|
||||
yield return companionDownload;
|
||||
if (companionDownload.Status != EOperationStatus.Succeeded)
|
||||
{
|
||||
onDone?.Invoke(null, $"资源包 {packageName} 下载失败: {companionDownload.Error}");
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_view?.SetProgress(0.95f);
|
||||
_view?.SetStatus("资源就绪");
|
||||
onDone?.Invoke(ctx, null);
|
||||
@@ -184,7 +240,7 @@ namespace AOT.Patch
|
||||
if (playMode == EPlayMode.EditorSimulateMode)
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
var buildResult = EditorSimulateBuildInvoker.Build(_settings.PackageName, (int)EBundleType.VirtualAssetBundle);
|
||||
var buildResult = EditorSimulateBuildInvoker.Build(package.PackageName, (int)EBundleType.VirtualAssetBundle);
|
||||
var options = new EditorSimulateModeOptions
|
||||
{
|
||||
EditorFileSystemParameters = FileSystemParameters.CreateDefaultEditorFileSystemParameters(buildResult.PackageRootDirectory)
|
||||
|
||||
@@ -13,6 +13,8 @@ namespace AOT.Patch
|
||||
public string[] packages = { "DefaultPackage" };
|
||||
public bool forceUpdate;
|
||||
public string notice = "";
|
||||
/// <summary>兼容旧版本字段;当前 UI 仅支持 candy,其他主题值回退到 Candy。</summary>
|
||||
public string uiTheme;
|
||||
|
||||
public static VersionInfo FromJson(string json)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f7dfe7e829d85904e8f1f4c05c9b65a0
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,139 @@
|
||||
using UnityEngine;
|
||||
using System.Linq;
|
||||
|
||||
/// <summary>
|
||||
/// 角色验收场景:对带 Animator 且含 int 参数 Motion 的单位轮播 Idle/Walk/Attack。
|
||||
/// Edit Mode(Scene 视图)与 Play Mode 均可预览。
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
[ExecuteAlways]
|
||||
public sealed class CharacterReviewAnimCycle : MonoBehaviour
|
||||
{
|
||||
[SerializeField] float secondsPerMotion = 2.8f;
|
||||
[SerializeField] string motionParam = "Motion";
|
||||
[SerializeField] string[] motionNames = { "Idle", "Walk", "Attack" };
|
||||
[SerializeField] int fixedMotion = -1;
|
||||
|
||||
Animator _animator;
|
||||
TextMesh _label;
|
||||
string _baseLabel;
|
||||
int _motionIndex;
|
||||
float _elapsed;
|
||||
double _lastEditorTime = -1;
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
CacheRefs();
|
||||
_elapsed = 0f;
|
||||
_motionIndex = 0;
|
||||
ApplyMotion(fixedMotion >= 0 ? fixedMotion : 0);
|
||||
#if UNITY_EDITOR
|
||||
if (!Application.isPlaying)
|
||||
{
|
||||
_lastEditorTime = UnityEditor.EditorApplication.timeSinceStartup;
|
||||
UnityEditor.EditorApplication.update -= EditorTick;
|
||||
UnityEditor.EditorApplication.update += EditorTick;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
UnityEditor.EditorApplication.update -= EditorTick;
|
||||
#endif
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (!Application.isPlaying) return;
|
||||
Tick(Time.deltaTime);
|
||||
UpdateStateLabel();
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
void EditorTick()
|
||||
{
|
||||
if (this == null || Application.isPlaying || !isActiveAndEnabled) return;
|
||||
double now = UnityEditor.EditorApplication.timeSinceStartup;
|
||||
float dt = _lastEditorTime < 0 ? 0.016f : (float)(now - _lastEditorTime);
|
||||
_lastEditorTime = now;
|
||||
if (dt <= 0f) return;
|
||||
if (dt > 0.1f) dt = 0.033f;
|
||||
Tick(dt);
|
||||
if (_animator != null && _animator.isActiveAndEnabled)
|
||||
_animator.Update(dt);
|
||||
UpdateStateLabel();
|
||||
UnityEditor.SceneView.RepaintAll();
|
||||
}
|
||||
#endif
|
||||
|
||||
void CacheRefs()
|
||||
{
|
||||
if (_animator == null)
|
||||
_animator = GetComponentInChildren<Animator>(true);
|
||||
if (_label == null)
|
||||
{
|
||||
var labelTf = transform.Find("NameLabel");
|
||||
if (labelTf != null) _label = labelTf.GetComponent<TextMesh>();
|
||||
}
|
||||
if (_label != null && string.IsNullOrEmpty(_baseLabel))
|
||||
{
|
||||
var t = _label.text ?? "";
|
||||
var cut = t.IndexOf(" [", System.StringComparison.Ordinal);
|
||||
_baseLabel = cut >= 0 ? t.Substring(0, cut) : t;
|
||||
}
|
||||
}
|
||||
|
||||
void Tick(float dt)
|
||||
{
|
||||
CacheRefs();
|
||||
if (_animator == null || _animator.runtimeAnimatorController == null) return;
|
||||
if (!HasMotionParam()) return;
|
||||
|
||||
_elapsed += dt;
|
||||
if (_elapsed < Mathf.Max(0.5f, secondsPerMotion)) return;
|
||||
_elapsed = 0f;
|
||||
int count = motionNames != null && motionNames.Length > 0 ? motionNames.Length : 3;
|
||||
_motionIndex = fixedMotion >= 0 ? fixedMotion : (_motionIndex + 1) % count;
|
||||
ApplyMotion(_motionIndex);
|
||||
}
|
||||
|
||||
bool HasMotionParam()
|
||||
{
|
||||
if (_animator == null) return false;
|
||||
foreach (var p in _animator.parameters)
|
||||
if (p.name == motionParam && p.type == AnimatorControllerParameterType.Int)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
void ApplyMotion(int index)
|
||||
{
|
||||
if (_animator == null) return;
|
||||
string selected = motionNames != null && index >= 0 && index < motionNames.Length
|
||||
? motionNames[index] : index.ToString();
|
||||
bool attackTrigger = _animator.parameters.Any(p=>p.name=="Attack" && p.type==AnimatorControllerParameterType.Trigger);
|
||||
int value = selected == "Walk" ? 1 : selected == "Attack" && !attackTrigger ? 2 : 0;
|
||||
if (HasMotionParam())
|
||||
_animator.SetInteger(motionParam, value);
|
||||
if(selected=="Attack" && attackTrigger) _animator.SetTrigger("Attack");
|
||||
if (_label != null)
|
||||
{
|
||||
string name = motionNames != null && index >= 0 && index < motionNames.Length
|
||||
? motionNames[index]
|
||||
: index.ToString();
|
||||
_label.text = string.IsNullOrEmpty(_baseLabel)
|
||||
? name
|
||||
: _baseLabel + " [" + name + "]";
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateStateLabel()
|
||||
{
|
||||
if(_animator==null || _label==null || _animator.runtimeAnimatorController==null) return;
|
||||
var state=_animator.GetCurrentAnimatorStateInfo(0);
|
||||
string name=state.IsName("Attack") ? "Attack" : state.IsName("Walk") ? "Walk" : "Idle";
|
||||
_label.text=_baseLabel+" ["+name+"]";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fdfd71c9f597ff64ca103fd303a24b13
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user