Add networked Fighter3D minigame
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 85085aa41f000aa4dbdb531fd2f5202b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,13 @@
|
||||
# Fighter3D
|
||||
|
||||
Networked 2.5D fighting mini game prototype.
|
||||
|
||||
## MVP Controls
|
||||
|
||||
- `A` / `LeftArrow`: move left
|
||||
- `D` / `RightArrow`: move right
|
||||
- `S` / `DownArrow` / `LeftShift`: guard
|
||||
- `J`: light attack
|
||||
- `K`: heavy attack
|
||||
|
||||
The server room is authoritative for combat state, damage, block, KO, timeout, and settlement.
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 55544ae8081da7b48835dd846c66ff7d
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"gameId": "fighter3d",
|
||||
"serverSrcDir": "Fighter3D",
|
||||
"serverProject": "Fighter3D.Server/Fighter3D.Server.csproj",
|
||||
"serverAssembly": "Fighter3D.Server.dll",
|
||||
"serverEntryType": "Fighter3D.Server.FighterServerRoom",
|
||||
"clientProject": "Client/HotUpdateGames/Fighter3D.Client/Fighter3D.Client.csproj",
|
||||
"coreDll": "Fighter3D.Core.dll",
|
||||
"clientDll": "Fighter3D.Client.dll",
|
||||
"clientEntryType": "Fighter3D.Client.FighterGameClient",
|
||||
"minFrameworkVersion": 1,
|
||||
"playerCount": 2,
|
||||
"tickRateHz": 20
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e0ac72934f4783e4f88a7564bac49bd6
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6eb52d097b913dd4a9bff20cb4b0247d
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8338e8813a2215943a9d02c93539a5a8
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f43bad7e65f7b9242a7dfcaa0aefe943
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"type": "fighter3d",
|
||||
"version": 1,
|
||||
"buffs": []
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: eed808c579abf7c46be3ece4492e802c
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"type": "fighter3d",
|
||||
"version": 1,
|
||||
"flyObjects": [
|
||||
{
|
||||
"id": "fighter3d_blade_wave",
|
||||
"duration": 1.2,
|
||||
"movement": { "kind": "Line", "speed": 6.0 },
|
||||
"collision": { "shape": "Sphere", "radius": 0.4, "hitLimit": 1 },
|
||||
"timeline": [
|
||||
{ "time": 0.0, "kind": "Effect", "prefab": "Assets/Game/Art/Effect/Prefab/skill/9110009_zhujue_quantao_skill02d_1.prefab", "duration": 1.2 }
|
||||
],
|
||||
"logicEffects": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1223a987b29ef3b4a9dd4ec31da27697
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"type": "fighter3d",
|
||||
"version": 1,
|
||||
"behaviors": [
|
||||
{
|
||||
"id": "fighter3d_light",
|
||||
"duration": 0.45,
|
||||
"canBeInterrupted": true,
|
||||
"input": "DirectionOrTarget",
|
||||
"timeline": [
|
||||
{ "time": 0.0, "kind": "Animation", "animation": "140_skill01a_qt", "duration": 0.45 },
|
||||
{ "time": 0.12, "kind": "Effect", "prefab": "Assets/Game/Art/Effect/Prefab/skill/9110004_zhujue_quantao_skill01a.prefab", "duration": 1.0 }
|
||||
],
|
||||
"logicEffects": []
|
||||
},
|
||||
{
|
||||
"id": "fighter3d_heavy",
|
||||
"duration": 0.75,
|
||||
"canBeInterrupted": true,
|
||||
"input": "DirectionOrTarget",
|
||||
"timeline": [
|
||||
{ "time": 0.0, "kind": "Animation", "animation": "140_skill02a_qt", "duration": 0.75 },
|
||||
{ "time": 0.18, "kind": "Projectile", "flyObjectId": "fighter3d_blade_wave" }
|
||||
],
|
||||
"logicEffects": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 24e933ae77efcdc48a59b0db7fcb1cf5
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8e30407f2677042458c0ab4703890606
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 64b9b67b84ee6ae4c94dc2c93a97361d
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cff69aff2bb11fc4f8e323dac4b4231f
|
||||
PrefabImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,340 @@
|
||||
using Fighter3D.Core;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using XGame.MiniGame;
|
||||
using XWorld.Framework;
|
||||
using XWorld.Framework.Protocol;
|
||||
|
||||
namespace Fighter3D.Client
|
||||
{
|
||||
public sealed class FighterGameClient : IGameClient
|
||||
{
|
||||
private const ushort InputOpcode = 1;
|
||||
private const ushort SnapshotOpcode = 100;
|
||||
private const string UiPrefabPath = "Assets/MiniGames/Fighter3D/res/UI/Prefab/UI_Fighter3D.prefab";
|
||||
private const string BehaviorCatalogType = "fighter3d";
|
||||
private const string BehaviorConfigRoot = "Assets/MiniGames/Fighter3D/res/Config/ActorBehavior";
|
||||
|
||||
private readonly FighterLogic _logic = new FighterLogic();
|
||||
private IGameClientCtx _ctx;
|
||||
private FighterState _state;
|
||||
private FighterHud _hud;
|
||||
private MiniGameActorPresentation _actors;
|
||||
private int _lastAxis = int.MinValue;
|
||||
private FighterButtons _lastButtons = (FighterButtons)255;
|
||||
|
||||
public void OnEnter(IGameClientCtx ctx)
|
||||
{
|
||||
_ctx = ctx;
|
||||
_actors = MiniGameActorPresentation.Create("Fighter3D_ActorPresentation", BehaviorCatalogType, BehaviorConfigRoot);
|
||||
RenderActors(_state);
|
||||
_ctx.Assets.Load(UiPrefabPath, OnPrefabLoaded);
|
||||
_ctx.Logger.Info("fighter3d client entered");
|
||||
}
|
||||
|
||||
public void OnNetMessage(NetMessage message)
|
||||
{
|
||||
if (message.Opcode != SnapshotOpcode)
|
||||
return;
|
||||
|
||||
_state = _logic.Decode(message.Payload);
|
||||
_hud?.Render(_state);
|
||||
RenderActors(_state);
|
||||
}
|
||||
|
||||
public void OnUpdate(float deltaTime)
|
||||
{
|
||||
if (_ctx == null)
|
||||
return;
|
||||
|
||||
int axis = ReadAxis();
|
||||
FighterButtons buttons = ReadButtons();
|
||||
if (axis != _lastAxis || buttons != _lastButtons)
|
||||
{
|
||||
SendControls(axis, buttons);
|
||||
_lastAxis = axis;
|
||||
_lastButtons = buttons;
|
||||
}
|
||||
|
||||
_hud?.Render(_state);
|
||||
RenderActors(_state);
|
||||
_actors?.Tick(deltaTime);
|
||||
}
|
||||
|
||||
public void OnExit()
|
||||
{
|
||||
_ctx?.Logger.Info("fighter3d client exited");
|
||||
_hud?.Destroy();
|
||||
_actors?.Destroy();
|
||||
_hud = null;
|
||||
_actors = null;
|
||||
_ctx = null;
|
||||
_state = null;
|
||||
_lastAxis = int.MinValue;
|
||||
_lastButtons = (FighterButtons)255;
|
||||
}
|
||||
|
||||
private void OnPrefabLoaded(object loaded)
|
||||
{
|
||||
if (_ctx == null)
|
||||
return;
|
||||
|
||||
GameObject root = loaded as GameObject;
|
||||
if (root != null)
|
||||
{
|
||||
root = UnityEngine.Object.Instantiate(root);
|
||||
root.name = "UI_Fighter3D";
|
||||
}
|
||||
else
|
||||
{
|
||||
_ctx.Logger.Info("fighter3d ui prefab missing, using runtime hud");
|
||||
root = FighterHud.CreateFallbackRoot();
|
||||
}
|
||||
|
||||
UnityEngine.Object.DontDestroyOnLoad(root);
|
||||
_hud = new FighterHud(root);
|
||||
_hud.Render(_state);
|
||||
}
|
||||
|
||||
private void RenderActors(FighterState state)
|
||||
{
|
||||
if (_actors == null)
|
||||
return;
|
||||
|
||||
if (state == null)
|
||||
{
|
||||
_actors.RenderActor(CreateActorFrame(0, -1f, 1, FighterAction.Idle, true));
|
||||
_actors.RenderActor(CreateActorFrame(1, 1f, -1, FighterAction.Idle, true));
|
||||
return;
|
||||
}
|
||||
|
||||
_actors.RenderActor(CreateActorFrame(state.Fighters[0], state.Phase != FighterPhase.Finished));
|
||||
_actors.RenderActor(CreateActorFrame(state.Fighters[1], state.Phase != FighterPhase.Finished));
|
||||
}
|
||||
|
||||
private void SendControls(int axis, FighterButtons buttons)
|
||||
{
|
||||
var writer = new PacketWriter();
|
||||
writer.WriteVarInt(axis);
|
||||
writer.WriteByte((byte)buttons);
|
||||
_ctx.Send(new NetMessage(InputOpcode, writer.ToArray()));
|
||||
}
|
||||
|
||||
private static int ReadAxis()
|
||||
{
|
||||
bool left = Input.GetKey(KeyCode.A) || Input.GetKey(KeyCode.LeftArrow);
|
||||
bool right = Input.GetKey(KeyCode.D) || Input.GetKey(KeyCode.RightArrow);
|
||||
if (left == right)
|
||||
return 0;
|
||||
return left ? -1 : 1;
|
||||
}
|
||||
|
||||
private static FighterButtons ReadButtons()
|
||||
{
|
||||
FighterButtons buttons = FighterButtons.None;
|
||||
if (Input.GetKey(KeyCode.S) || Input.GetKey(KeyCode.DownArrow) || Input.GetKey(KeyCode.LeftShift))
|
||||
buttons |= FighterButtons.Guard;
|
||||
if (Input.GetKey(KeyCode.J))
|
||||
buttons |= FighterButtons.Light;
|
||||
if (Input.GetKey(KeyCode.K))
|
||||
buttons |= FighterButtons.Heavy;
|
||||
return buttons;
|
||||
}
|
||||
|
||||
private static MiniGameActorFrame CreateActorFrame(FighterUnitState unit, bool running)
|
||||
{
|
||||
return CreateActorFrame(unit.Seat, unit.X, unit.Facing, unit.Action, running && unit.Hp > 0);
|
||||
}
|
||||
|
||||
private static MiniGameActorFrame CreateActorFrame(int seat, float x, int facing, FighterAction action, bool alive)
|
||||
{
|
||||
int actorId = seat + 1;
|
||||
return new MiniGameActorFrame
|
||||
{
|
||||
ActorId = actorId,
|
||||
Figure = actorId,
|
||||
TeamId = actorId,
|
||||
Position = new Vector3(x, action == FighterAction.Knockout ? 0.16f : 0f, 0f),
|
||||
Forward = facing >= 0 ? Vector3.right : Vector3.left,
|
||||
Moving = action == FighterAction.Moving,
|
||||
Alive = alive && action != FighterAction.Knockout,
|
||||
TriggerKey = action.ToString(),
|
||||
BehaviorId = GetBehaviorId(action)
|
||||
};
|
||||
}
|
||||
|
||||
private static string GetBehaviorId(FighterAction action)
|
||||
{
|
||||
if (action == FighterAction.LightAttack)
|
||||
return "fighter3d_light";
|
||||
if (action == FighterAction.HeavyAttack)
|
||||
return "fighter3d_heavy";
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class FighterHud
|
||||
{
|
||||
private readonly GameObject _root;
|
||||
private readonly HudText _title;
|
||||
private readonly HudText _leftText;
|
||||
private readonly HudText _rightText;
|
||||
private readonly Image _leftHp;
|
||||
private readonly Image _rightHp;
|
||||
|
||||
public FighterHud(GameObject root)
|
||||
{
|
||||
_root = root;
|
||||
_title = FindText("Txt_Title");
|
||||
_leftText = FindText("Txt_Left");
|
||||
_rightText = FindText("Txt_Right");
|
||||
_leftHp = Find<Image>("Img_LeftHpFill");
|
||||
_rightHp = Find<Image>("Img_RightHpFill");
|
||||
}
|
||||
|
||||
public void Render(FighterState state)
|
||||
{
|
||||
if (state == null)
|
||||
{
|
||||
_title.Set("Fighter3D waiting for snapshot");
|
||||
_leftText.Set("P1");
|
||||
_rightText.Set("P2");
|
||||
SetFill(_leftHp, 1f);
|
||||
SetFill(_rightHp, 1f);
|
||||
return;
|
||||
}
|
||||
|
||||
var left = state.Fighters[0];
|
||||
var right = state.Fighters[1];
|
||||
string title = state.Phase == FighterPhase.Finished
|
||||
? state.WinnerSeat == 2 ? "Draw" : "Winner Seat " + state.WinnerSeat
|
||||
: "Fighter3D " + state.ElapsedSeconds.ToString("0.0") + "s";
|
||||
|
||||
_title.Set(title);
|
||||
_leftText.Set("P1 HP " + left.Hp + " " + left.Action + " X " + left.X.ToString("0.00"));
|
||||
_rightText.Set("P2 HP " + right.Hp + " " + right.Action + " X " + right.X.ToString("0.00"));
|
||||
SetFill(_leftHp, left.Hp / 1000f);
|
||||
SetFill(_rightHp, right.Hp / 1000f);
|
||||
}
|
||||
|
||||
public void Destroy()
|
||||
{
|
||||
if (_root != null)
|
||||
UnityEngine.Object.Destroy(_root);
|
||||
}
|
||||
|
||||
public static GameObject CreateFallbackRoot()
|
||||
{
|
||||
var root = new GameObject("UI_Fighter3D_Runtime");
|
||||
var canvas = root.AddComponent<Canvas>();
|
||||
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||||
root.AddComponent<CanvasScaler>().referenceResolution = new Vector2(2048, 1024);
|
||||
root.AddComponent<GraphicRaycaster>();
|
||||
|
||||
CreateText(root.transform, "Txt_Title", new Vector2(0, 430), new Vector2(900, 70), 34, TextAnchor.MiddleCenter);
|
||||
CreateText(root.transform, "Txt_Left", new Vector2(-520, 340), new Vector2(700, 60), 28, TextAnchor.MiddleLeft);
|
||||
CreateText(root.transform, "Txt_Right", new Vector2(520, 340), new Vector2(700, 60), 28, TextAnchor.MiddleRight);
|
||||
CreateHpBar(root.transform, "LeftHp", new Vector2(-520, 285));
|
||||
CreateHpBar(root.transform, "RightHp", new Vector2(520, 285));
|
||||
CreateText(root.transform, "Txt_Help", new Vector2(0, -420), new Vector2(900, 50), 22, TextAnchor.MiddleCenter)
|
||||
.text = "A/D move S guard J light K heavy";
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
private T Find<T>(string name) where T : Component
|
||||
{
|
||||
Transform child = FindChild(_root.transform, name);
|
||||
return child == null ? null : child.GetComponent<T>();
|
||||
}
|
||||
|
||||
private HudText FindText(string name)
|
||||
{
|
||||
Transform child = FindChild(_root.transform, name);
|
||||
return child == null ? HudText.Empty : new HudText(child.GetComponent<Text>(), child.GetComponent<TMP_Text>());
|
||||
}
|
||||
|
||||
private static Transform FindChild(Transform root, string name)
|
||||
{
|
||||
if (root.name == name)
|
||||
return root;
|
||||
|
||||
for (int i = 0; i < root.childCount; i++)
|
||||
{
|
||||
Transform found = FindChild(root.GetChild(i), name);
|
||||
if (found != null)
|
||||
return found;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Text CreateText(Transform parent, string name, Vector2 anchoredPosition, Vector2 size, int fontSize, TextAnchor alignment)
|
||||
{
|
||||
var go = new GameObject(name);
|
||||
go.transform.SetParent(parent, false);
|
||||
var rect = go.AddComponent<RectTransform>();
|
||||
rect.sizeDelta = size;
|
||||
rect.anchoredPosition = anchoredPosition;
|
||||
|
||||
var text = go.AddComponent<Text>();
|
||||
text.font = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
|
||||
text.fontSize = fontSize;
|
||||
text.alignment = alignment;
|
||||
text.color = Color.white;
|
||||
return text;
|
||||
}
|
||||
|
||||
private static void CreateHpBar(Transform parent, string name, Vector2 anchoredPosition)
|
||||
{
|
||||
var bg = new GameObject("Img_" + name + "Bg");
|
||||
bg.transform.SetParent(parent, false);
|
||||
var bgRect = bg.AddComponent<RectTransform>();
|
||||
bgRect.sizeDelta = new Vector2(700, 30);
|
||||
bgRect.anchoredPosition = anchoredPosition;
|
||||
var bgImage = bg.AddComponent<Image>();
|
||||
bgImage.color = new Color(0.12f, 0.12f, 0.12f, 0.85f);
|
||||
|
||||
var fill = new GameObject("Img_" + name + "Fill");
|
||||
fill.transform.SetParent(bg.transform, false);
|
||||
var fillRect = fill.AddComponent<RectTransform>();
|
||||
fillRect.anchorMin = new Vector2(0, 0);
|
||||
fillRect.anchorMax = new Vector2(1, 1);
|
||||
fillRect.offsetMin = Vector2.zero;
|
||||
fillRect.offsetMax = Vector2.zero;
|
||||
var fillImage = fill.AddComponent<Image>();
|
||||
fillImage.type = Image.Type.Filled;
|
||||
fillImage.fillMethod = Image.FillMethod.Horizontal;
|
||||
fillImage.color = name == "LeftHp" ? new Color(0.18f, 0.8f, 0.42f) : new Color(0.9f, 0.23f, 0.22f);
|
||||
}
|
||||
|
||||
private static void SetFill(Image image, float value)
|
||||
{
|
||||
if (image != null)
|
||||
image.fillAmount = Mathf.Clamp01(value);
|
||||
}
|
||||
|
||||
private readonly struct HudText
|
||||
{
|
||||
public static readonly HudText Empty = new HudText(null, null);
|
||||
|
||||
private readonly Text _legacy;
|
||||
private readonly TMP_Text _tmp;
|
||||
|
||||
public HudText(Text legacy, TMP_Text tmp)
|
||||
{
|
||||
_legacy = legacy;
|
||||
_tmp = tmp;
|
||||
}
|
||||
|
||||
public void Set(string value)
|
||||
{
|
||||
if (_legacy != null)
|
||||
_legacy.text = value;
|
||||
if (_tmp != null)
|
||||
_tmp.text = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using XWorld.Framework;
|
||||
using XWorld.Framework.Protocol;
|
||||
|
||||
namespace Fighter3D.Core
|
||||
{
|
||||
public sealed class FighterLogic : IGameLogic<FighterState, FighterInput, FighterEvent>
|
||||
{
|
||||
public const int TickRate = 60;
|
||||
public const float FixedDelta = 1f / TickRate;
|
||||
public const float StageHalfWidth = 4f;
|
||||
public const float RoundSeconds = 60f;
|
||||
|
||||
private const float MoveSpeed = 2.6f;
|
||||
private const int MaxHp = 1000;
|
||||
|
||||
public FighterState CreateInitial(RoomConfig config, IRandom random)
|
||||
{
|
||||
var state = new FighterState
|
||||
{
|
||||
Phase = FighterPhase.Running,
|
||||
WinnerSeat = -1
|
||||
};
|
||||
|
||||
InitFighter(state.Fighters[0], 0, -1f, 1);
|
||||
InitFighter(state.Fighters[1], 1, 1f, -1);
|
||||
return state;
|
||||
}
|
||||
|
||||
public StepResult<FighterState, FighterEvent> Step(FighterState state, FighterInput input, IRandom random)
|
||||
{
|
||||
var events = new List<FighterEvent>();
|
||||
if (state == null)
|
||||
state = CreateInitial(null, random);
|
||||
|
||||
if (input.Kind == FighterInputKind.Controls)
|
||||
{
|
||||
if (input.Seat >= 0 && input.Seat < state.Fighters.Length)
|
||||
{
|
||||
state.Fighters[input.Seat].ControlAxis = input.Axis < 0 ? -1 : input.Axis > 0 ? 1 : 0;
|
||||
state.Fighters[input.Seat].HeldButtons = input.Buttons;
|
||||
}
|
||||
|
||||
return new StepResult<FighterState, FighterEvent>(state, events);
|
||||
}
|
||||
|
||||
if (state.Phase == FighterPhase.Finished)
|
||||
return new StepResult<FighterState, FighterEvent>(state, events);
|
||||
|
||||
state.TickAccumulator += Math.Max(0f, input.Dt);
|
||||
while (state.TickAccumulator + 0.000001f >= FixedDelta && state.Phase == FighterPhase.Running)
|
||||
{
|
||||
state.TickAccumulator -= FixedDelta;
|
||||
AdvanceFixedTick(state, events);
|
||||
}
|
||||
|
||||
return new StepResult<FighterState, FighterEvent>(state, events);
|
||||
}
|
||||
|
||||
public byte[] Encode(FighterState state)
|
||||
{
|
||||
var writer = new PacketWriter(96);
|
||||
writer.WriteByte((byte)state.Phase);
|
||||
writer.WriteVarInt(state.Tick);
|
||||
writer.WriteSingle(state.TickAccumulator);
|
||||
writer.WriteSingle(state.ElapsedSeconds);
|
||||
writer.WriteVarInt(state.WinnerSeat);
|
||||
|
||||
for (int i = 0; i < 2; i++)
|
||||
WriteFighter(writer, state.Fighters[i]);
|
||||
|
||||
return writer.ToArray();
|
||||
}
|
||||
|
||||
public FighterState Decode(byte[] data)
|
||||
{
|
||||
var reader = new PacketReader(data);
|
||||
var state = new FighterState
|
||||
{
|
||||
Phase = (FighterPhase)reader.ReadByte(),
|
||||
Tick = reader.ReadVarInt(),
|
||||
TickAccumulator = reader.ReadSingle(),
|
||||
ElapsedSeconds = reader.ReadSingle(),
|
||||
WinnerSeat = reader.ReadVarInt()
|
||||
};
|
||||
|
||||
for (int i = 0; i < 2; i++)
|
||||
ReadFighter(reader, state.Fighters[i]);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
private static void InitFighter(FighterUnitState fighter, int seat, float x, int facing)
|
||||
{
|
||||
fighter.Seat = seat;
|
||||
fighter.X = x;
|
||||
fighter.Hp = MaxHp;
|
||||
fighter.Facing = facing;
|
||||
fighter.Action = FighterAction.Idle;
|
||||
fighter.ActionFrame = 0;
|
||||
fighter.StunTicks = 0;
|
||||
fighter.ControlAxis = 0;
|
||||
fighter.HeldButtons = FighterButtons.None;
|
||||
fighter.HasHit = false;
|
||||
}
|
||||
|
||||
private static void AdvanceFixedTick(FighterState state, List<FighterEvent> events)
|
||||
{
|
||||
state.Tick++;
|
||||
state.ElapsedSeconds += FixedDelta;
|
||||
UpdateFacing(state);
|
||||
|
||||
for (int i = 0; i < 2; i++)
|
||||
AdvanceFighter(state, i, events);
|
||||
|
||||
ResolveTimeout(state, events);
|
||||
}
|
||||
|
||||
private static void UpdateFacing(FighterState state)
|
||||
{
|
||||
var a = state.Fighters[0];
|
||||
var b = state.Fighters[1];
|
||||
if (Math.Abs(a.X - b.X) < 0.0001f)
|
||||
return;
|
||||
|
||||
a.Facing = a.X < b.X ? 1 : -1;
|
||||
b.Facing = b.X < a.X ? 1 : -1;
|
||||
}
|
||||
|
||||
private static void AdvanceFighter(FighterState state, int seat, List<FighterEvent> events)
|
||||
{
|
||||
var fighter = state.Fighters[seat];
|
||||
if (fighter.Action == FighterAction.Knockout)
|
||||
return;
|
||||
|
||||
if (fighter.Action == FighterAction.HitStun || fighter.Action == FighterAction.BlockStun)
|
||||
{
|
||||
fighter.ActionFrame++;
|
||||
fighter.StunTicks--;
|
||||
if (fighter.StunTicks <= 0)
|
||||
EnterAction(fighter, FighterAction.Idle);
|
||||
return;
|
||||
}
|
||||
|
||||
if (fighter.Action == FighterAction.LightAttack || fighter.Action == FighterAction.HeavyAttack)
|
||||
{
|
||||
fighter.ActionFrame++;
|
||||
TryResolveAttack(state, fighter, events);
|
||||
if (fighter.ActionFrame >= TotalFrames(fighter.Action))
|
||||
EnterAction(fighter, FighterAction.Idle);
|
||||
return;
|
||||
}
|
||||
|
||||
if ((fighter.HeldButtons & FighterButtons.Heavy) != 0)
|
||||
{
|
||||
EnterAction(fighter, FighterAction.HeavyAttack);
|
||||
return;
|
||||
}
|
||||
|
||||
if ((fighter.HeldButtons & FighterButtons.Light) != 0)
|
||||
{
|
||||
EnterAction(fighter, FighterAction.LightAttack);
|
||||
return;
|
||||
}
|
||||
|
||||
if (fighter.ControlAxis != 0)
|
||||
{
|
||||
fighter.Action = FighterAction.Moving;
|
||||
fighter.X = Clamp(fighter.X + fighter.ControlAxis * MoveSpeed * FixedDelta, -StageHalfWidth, StageHalfWidth);
|
||||
}
|
||||
else
|
||||
{
|
||||
fighter.Action = FighterAction.Idle;
|
||||
}
|
||||
}
|
||||
|
||||
private static void TryResolveAttack(FighterState state, FighterUnitState attacker, List<FighterEvent> events)
|
||||
{
|
||||
if (attacker.HasHit || !IsActive(attacker.Action, attacker.ActionFrame))
|
||||
return;
|
||||
|
||||
var defender = state.Fighters[1 - attacker.Seat];
|
||||
if (defender.Action == FighterAction.Knockout)
|
||||
return;
|
||||
|
||||
float distance = Math.Abs(defender.X - attacker.X);
|
||||
if (distance > Range(attacker.Action))
|
||||
return;
|
||||
|
||||
if (Math.Sign(defender.X - attacker.X) != attacker.Facing)
|
||||
return;
|
||||
|
||||
attacker.HasHit = true;
|
||||
if (IsGuardingAway(defender))
|
||||
{
|
||||
EnterStun(defender, FighterAction.BlockStun, BlockStun(attacker.Action));
|
||||
Push(defender, attacker.Facing, BlockPush(attacker.Action));
|
||||
events.Add(new FighterEvent { Kind = FighterEventKind.Block, AttackerSeat = attacker.Seat, DefenderSeat = defender.Seat });
|
||||
return;
|
||||
}
|
||||
|
||||
defender.Hp = Math.Max(0, defender.Hp - Damage(attacker.Action));
|
||||
Push(defender, attacker.Facing, HitPush(attacker.Action));
|
||||
if (defender.Hp <= 0)
|
||||
{
|
||||
EnterAction(defender, FighterAction.Knockout);
|
||||
state.Phase = FighterPhase.Finished;
|
||||
state.WinnerSeat = attacker.Seat;
|
||||
events.Add(new FighterEvent { Kind = FighterEventKind.Finished, AttackerSeat = attacker.Seat, DefenderSeat = defender.Seat, WinnerSeat = attacker.Seat });
|
||||
}
|
||||
else
|
||||
{
|
||||
EnterStun(defender, FighterAction.HitStun, HitStun(attacker.Action));
|
||||
events.Add(new FighterEvent { Kind = FighterEventKind.Hit, AttackerSeat = attacker.Seat, DefenderSeat = defender.Seat });
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsGuardingAway(FighterUnitState defender)
|
||||
{
|
||||
return (defender.HeldButtons & FighterButtons.Guard) != 0
|
||||
|| defender.ControlAxis == -defender.Facing;
|
||||
}
|
||||
|
||||
private static void EnterAction(FighterUnitState fighter, FighterAction action)
|
||||
{
|
||||
fighter.Action = action;
|
||||
fighter.ActionFrame = 0;
|
||||
fighter.StunTicks = 0;
|
||||
fighter.HasHit = false;
|
||||
}
|
||||
|
||||
private static void EnterStun(FighterUnitState fighter, FighterAction action, int stunTicks)
|
||||
{
|
||||
fighter.Action = action;
|
||||
fighter.ActionFrame = 0;
|
||||
fighter.StunTicks = stunTicks;
|
||||
fighter.HasHit = false;
|
||||
}
|
||||
|
||||
private static void Push(FighterUnitState fighter, int direction, float distance)
|
||||
{
|
||||
fighter.X = Clamp(fighter.X + direction * distance, -StageHalfWidth, StageHalfWidth);
|
||||
}
|
||||
|
||||
private static void ResolveTimeout(FighterState state, List<FighterEvent> events)
|
||||
{
|
||||
float battleSeconds = state.Tick * FixedDelta;
|
||||
if (state.Phase == FighterPhase.Finished || battleSeconds + 0.0001f < RoundSeconds)
|
||||
return;
|
||||
|
||||
int hp0 = state.Fighters[0].Hp;
|
||||
int hp1 = state.Fighters[1].Hp;
|
||||
state.Phase = FighterPhase.Finished;
|
||||
state.WinnerSeat = hp0 == hp1 ? 2 : hp0 > hp1 ? 0 : 1;
|
||||
events.Add(new FighterEvent { Kind = FighterEventKind.Finished, WinnerSeat = state.WinnerSeat });
|
||||
}
|
||||
|
||||
private static bool IsActive(FighterAction action, int frame)
|
||||
{
|
||||
return frame >= Startup(action) && frame < Startup(action) + Active(action);
|
||||
}
|
||||
|
||||
private static int Startup(FighterAction action) => action == FighterAction.HeavyAttack ? 7 : 4;
|
||||
private static int Active(FighterAction action) => action == FighterAction.HeavyAttack ? 4 : 3;
|
||||
private static int TotalFrames(FighterAction action) => action == FighterAction.HeavyAttack ? 29 : 15;
|
||||
private static int Damage(FighterAction action) => action == FighterAction.HeavyAttack ? 250 : 60;
|
||||
private static int HitStun(FighterAction action) => action == FighterAction.HeavyAttack ? 18 : 12;
|
||||
private static int BlockStun(FighterAction action) => action == FighterAction.HeavyAttack ? 14 : 8;
|
||||
private static float Range(FighterAction action) => action == FighterAction.HeavyAttack ? 2.2f : 1.15f;
|
||||
private static float HitPush(FighterAction action) => action == FighterAction.HeavyAttack ? 0.04f : 0.02f;
|
||||
private static float BlockPush(FighterAction action) => action == FighterAction.HeavyAttack ? 0.03f : 0.015f;
|
||||
|
||||
private static float Clamp(float value, float min, float max)
|
||||
{
|
||||
if (value < min) return min;
|
||||
if (value > max) return max;
|
||||
return value;
|
||||
}
|
||||
|
||||
private static void WriteFighter(PacketWriter writer, FighterUnitState fighter)
|
||||
{
|
||||
writer.WriteVarInt(fighter.Seat);
|
||||
writer.WriteSingle(fighter.X);
|
||||
writer.WriteVarInt(fighter.Hp);
|
||||
writer.WriteVarInt(fighter.Facing);
|
||||
writer.WriteByte((byte)fighter.Action);
|
||||
writer.WriteVarInt(fighter.ActionFrame);
|
||||
writer.WriteVarInt(fighter.StunTicks);
|
||||
writer.WriteVarInt(fighter.ControlAxis);
|
||||
writer.WriteByte((byte)fighter.HeldButtons);
|
||||
writer.WriteBool(fighter.HasHit);
|
||||
}
|
||||
|
||||
private static void ReadFighter(PacketReader reader, FighterUnitState fighter)
|
||||
{
|
||||
fighter.Seat = reader.ReadVarInt();
|
||||
fighter.X = reader.ReadSingle();
|
||||
fighter.Hp = reader.ReadVarInt();
|
||||
fighter.Facing = reader.ReadVarInt();
|
||||
fighter.Action = (FighterAction)reader.ReadByte();
|
||||
fighter.ActionFrame = reader.ReadVarInt();
|
||||
fighter.StunTicks = reader.ReadVarInt();
|
||||
fighter.ControlAxis = reader.ReadVarInt();
|
||||
fighter.HeldButtons = (FighterButtons)reader.ReadByte();
|
||||
fighter.HasHit = reader.ReadBool();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
using System;
|
||||
|
||||
namespace Fighter3D.Core
|
||||
{
|
||||
public enum FighterPhase : byte
|
||||
{
|
||||
Running = 0,
|
||||
Finished = 1
|
||||
}
|
||||
|
||||
public enum FighterAction : byte
|
||||
{
|
||||
Idle = 0,
|
||||
Moving = 1,
|
||||
LightAttack = 2,
|
||||
HeavyAttack = 3,
|
||||
HitStun = 4,
|
||||
BlockStun = 5,
|
||||
Knockout = 6
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum FighterButtons : byte
|
||||
{
|
||||
None = 0,
|
||||
Guard = 1,
|
||||
Light = 2,
|
||||
Heavy = 4
|
||||
}
|
||||
|
||||
public enum FighterInputKind : byte
|
||||
{
|
||||
Tick = 0,
|
||||
Controls = 1
|
||||
}
|
||||
|
||||
public enum FighterEventKind : byte
|
||||
{
|
||||
Hit = 0,
|
||||
Block = 1,
|
||||
Finished = 2
|
||||
}
|
||||
|
||||
public sealed class FighterUnitState
|
||||
{
|
||||
public int Seat;
|
||||
public float X;
|
||||
public int Hp;
|
||||
public int Facing;
|
||||
public FighterAction Action;
|
||||
public int ActionFrame;
|
||||
public int StunTicks;
|
||||
public int ControlAxis;
|
||||
public FighterButtons HeldButtons;
|
||||
public bool HasHit;
|
||||
}
|
||||
|
||||
public sealed class FighterState
|
||||
{
|
||||
public int Tick;
|
||||
public float TickAccumulator;
|
||||
public float ElapsedSeconds;
|
||||
public FighterPhase Phase;
|
||||
public int WinnerSeat = -1;
|
||||
public FighterUnitState[] Fighters =
|
||||
{
|
||||
new FighterUnitState { Seat = 0 },
|
||||
new FighterUnitState { Seat = 1 }
|
||||
};
|
||||
}
|
||||
|
||||
public struct FighterInput
|
||||
{
|
||||
public FighterInputKind Kind;
|
||||
public int Seat;
|
||||
public int Axis;
|
||||
public FighterButtons Buttons;
|
||||
public float Dt;
|
||||
|
||||
public static FighterInput Tick(float dt)
|
||||
{
|
||||
return new FighterInput { Kind = FighterInputKind.Tick, Dt = dt };
|
||||
}
|
||||
|
||||
public static FighterInput Controls(int seat, int axis, FighterButtons buttons)
|
||||
{
|
||||
return new FighterInput
|
||||
{
|
||||
Kind = FighterInputKind.Controls,
|
||||
Seat = seat,
|
||||
Axis = axis < 0 ? -1 : axis > 0 ? 1 : 0,
|
||||
Buttons = buttons
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public struct FighterEvent
|
||||
{
|
||||
public FighterEventKind Kind;
|
||||
public int AttackerSeat;
|
||||
public int DefenderSeat;
|
||||
public int WinnerSeat;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user