-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathCharacterStats.tsx
148 lines (128 loc) · 5.58 KB
/
CharacterStats.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
import { useEventfulState } from "onejs"
import { Dom } from "OneJS/Dom"
import { h } from "preact"
import { forwardRef } from "preact/compat"
import { MutableRef, useState } from "preact/hooks"
import { useEffect, useRef } from "preact/hooks"
import { Easing, Tween } from "tweenjs"
import { Color, Vector2 } from "UnityEngine"
import { MeshGenerationContext, LineJoin, LineCap, Painter2D } from "UnityEngine/UIElements"
import { CharacterManager } from "./charman"
/**
* UPDATE: You can ignore this old note for OneJS V2 as it has achieved
* zero-allocation interop. So you can use the vector api every frame from
* JS to your heart's content (super fast performance with 0 GC.alloc).
*
* Note: This health bar here is a good example of how you'd want to approach
* performance tuning (since using vector api every frame can be quite heavy).
*
* Recommended workflow (for CPU-bound UI animation):
*
* 1) Rapid prototype everything here in TS with states and dom re-renders.
* 2) Identify areas where UI state change is very frequent (i.e. every frame) and
* turn it into imperative code.
* 3) If more performance is needed, convert the TS code into a custom VisualElement
* in C#.
* 4) If even more performance is needed, consider pooling animations with bursted jobs.
*
* For majority of UI code where state change is infrequent (not every frame), you can
* just stop at step 1). In this particular sample, we achieved acceptable desktop
* performance at step 2) after converting some declarative code into imperative. In
* practice, you'd want to go for step 3) for this kind of health bar animation.
*/
const Bar = forwardRef(({ }: {}, ref: MutableRef<Dom>) => {
useEffect(() => {
ref.current.ve.generateVisualContent = onGenerateVisualContent
ref.current.ve.MarkDirtyRepaint()
}, [])
function onGenerateVisualContent(mgc: MeshGenerationContext) {
let paint2D = mgc.painter2D
let { health, maxHealth } = lerped
let width = ref.current.ve.resolvedStyle.width
let height = ref.current.ve.resolvedStyle.height
let numSegments = Math.ceil(maxHealth / interval)
let numGaps = numSegments - 1
let intrinsicWidth = width - numGaps * (r + g + r)
let fullSegmentWidth = intrinsicWidth / maxHealth * interval
PaintBar(health, new Color(1, 1, 1))
function PaintBar(hp: number, color: Color) {
let ns = Math.ceil(hp / interval) // Num Segments
let nfs = Math.floor(hp / interval) // Num Full Segments
let i = 0
for (i = 0; i < nfs; i++) {
paintSegment(paint2D, i * (fullSegmentWidth + (r + g + r)), fullSegmentWidth, height, color)
}
if (ns != nfs) {
let lastSegmentWidth = ((hp % interval) / interval) * fullSegmentWidth
paintSegment(paint2D, i * (fullSegmentWidth + (r + g + r)), lastSegmentWidth, height, color)
}
}
}
return (
<div ref={ref} class="w-full h-full"></div>
)
})
interface LerpedNums {
maxHealth: number
health: number
}
let lerped: LerpedNums
let tween: Tween<LerpedNums>
export const CharacterStats = () => {
const barRef = useRef<Dom>()
const healthRef = useRef<Dom>()
const maxHealthRef = useRef<Dom>()
var charman = require("charman") as CharacterManager
const [health, setHealth] = useEventfulState(charman, "Health")
const [maxHealth, setMaxHealth] = useEventfulState(charman, "MaxHealth")
if (typeof lerped === "undefined")
lerped = { health: charman.Health, maxHealth: charman.MaxHealth }
useEffect(() => {
if (typeof tween !== "undefined")
tween.stop()
let l = { health: lerped.health, maxHealth: lerped.maxHealth }
tween = new Tween(l).to({ maxHealth, health }, 800)
.easing(Easing.Quadratic.InOut).onUpdate(() => {
lerped.health = l.health
lerped.maxHealth = l.maxHealth
barRef.current.ve.MarkDirtyRepaint()
healthRef.current.ve[0].text = Math.round(l.health)
maxHealthRef.current.ve[0].text = Math.round(l.maxHealth)
}).start()
}, [health, maxHealth])
return (
<div class="flex-row">
<image class="w-40 h-40" image={resource.loadImage(__dirname + "/resources/portrait.png")} />
<div class="flex-col">
<div class="w-96 h-20 flex-row items-end text-xl bold text-white pb-1">
<div ref={healthRef} class="text-4xl" style={{ bottom: -6 }}>{Math.round(lerped.health)}</div>
<div class="text-gray-200 px-1">/</div>
<div ref={maxHealthRef} class="">{maxHealth}</div>
</div>
<div class="grow pt-2 pb-10">
<Bar ref={barRef} />
</div>
</div>
</div>
)
}
const r = 2 // rounded radius
const g = 2 // gap width
const shift = 4
const interval = 25
function paintSegment(paint2D: Painter2D, start: number, width: number, height: number, color: Color) {
paint2D.strokeColor = color;
paint2D.fillColor = color;
paint2D.lineWidth = 4;
paint2D.lineJoin = LineJoin.Round
paint2D.lineCap = LineCap.Round
paint2D.BeginPath()
paint2D.MoveTo(new Vector2(start + shift, 0))
paint2D.LineTo(new Vector2(start + width + shift, 0))
paint2D.LineTo(new Vector2(start + width - shift, height))
paint2D.LineTo(new Vector2(start - shift, height))
paint2D.LineTo(new Vector2(start + shift, 0))
paint2D.ClosePath();
paint2D.Fill();
paint2D.Stroke();
}