From 532e65e6fbae4e53fd47429f195f6cf7423a53bf Mon Sep 17 00:00:00 2001 From: starmen1265 Date: Sun, 15 Dec 2024 00:59:23 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0ReactUI-=E6=9C=AA=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=EF=BC=8C=E5=BE=85=E7=BB=A7=E7=BB=AD=E8=B0=83=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TypeScript/entry.ts | 10 +- TypeScript/react-umg/react-umg.ts | 293 ++++++++++++++++++++++++++++++ TypeScript/ui/index.tsx | 52 ++++++ TypeScript/ui/ui-components.tsx | 50 +++++ 4 files changed, 403 insertions(+), 2 deletions(-) create mode 100644 TypeScript/react-umg/react-umg.ts create mode 100644 TypeScript/ui/index.tsx create mode 100644 TypeScript/ui/ui-components.tsx diff --git a/TypeScript/entry.ts b/TypeScript/entry.ts index 07c9b5c..3eec0b1 100644 --- a/TypeScript/entry.ts +++ b/TypeScript/entry.ts @@ -1,4 +1,10 @@ import * as UE from 'ue' -import {argv} from 'puerts' +import {argv} from 'puerts'; +import {ReactUMG} from 'react-umg'; +import * as UI from './ui'; -console.log("---------------------------------0---------------------") +console.log('--------------------------0----------------------') +//用React来写UI +let world = (argv.getByName("GameInstance") as UE.GameInstance).GetWorld(); +ReactUMG.init(world); +UI.Load(); diff --git a/TypeScript/react-umg/react-umg.ts b/TypeScript/react-umg/react-umg.ts new file mode 100644 index 0000000..c8c3748 --- /dev/null +++ b/TypeScript/react-umg/react-umg.ts @@ -0,0 +1,293 @@ +import * as Reconciler from 'react-reconciler' +import * as puerts from 'puerts' +import * as UE from 'ue' + +let world: UE.World; + +function deepEquals(x: any, y: any) { + if ( x === y ) return true; + + if ( ! ( x instanceof Object ) || ! ( y instanceof Object ) ) return false; + + for (var p in x) { // all x[p] in y + if (p == 'children' || p == 'Slot') continue; + if (!deepEquals(x[p], y[p])) return false; + } + + for (var p in y) { + if (p == 'children' || p == 'Slot') continue; + if (!x.hasOwnProperty(p)) return false; + } + + return true; +} + +declare const exports: {lazyloadComponents:{}} + +class UEWidget { + type: string; + callbackRemovers: {[key: string] : () => void}; + nativePtr: UE.Widget; + slot: any; + nativeSlotPtr: UE.PanelSlot; + + constructor (type: string, props: any) { + this.type = type; + this.callbackRemovers = {}; + + try { + this.init(type, props); + } catch(e) { + console.error("create " + type + " throw " + e); + } + } + + init(type: string, props: any) { + let classPath = exports.lazyloadComponents[type]; + if (classPath) { + //this.nativePtr = asyncUIManager.CreateComponentByClassPathName(classPath); + this.nativePtr = UE.NewObject(UE.Class.Load(classPath)) as UE.Widget; + } else { + this.nativePtr = new UE[type](); + } + + let myProps = {}; + for (const key in props) { + let val = props[key]; + if (key == 'Slot') { + this.slot = val; + } else if (typeof val === 'function') { + this.bind(key, val); + } else if(key !== 'children') { + myProps[key] = val; + } + } + //console.log("UEWidget", type, JSON.stringify(myProps)) + puerts.merge(this.nativePtr, myProps); + //console.log(type + ' inited') + } + + bind(name: string, callback: Function) { + let nativePtr = this.nativePtr + let prop = nativePtr[name]; + if (typeof prop.Add === 'function') { + prop.Add(callback); + this.callbackRemovers[name] = () => { + prop.Remove(callback); + } + } else if (typeof prop.Bind == 'function') { + prop.Bind(callback); + this.callbackRemovers[name] = () => { + prop.Unbind(); + } + } else { + console.warn("unsupport callback " + name); + } + } + + update(oldProps: any, newProps: any) { + let myProps = {}; + let propChange = false; + for(var key in newProps) { + let oldProp = oldProps[key]; + let newProp = newProps[key]; + if (key != 'children' && oldProp != newProp) { + if (key == 'Slot') { + this.slot = newProp; + //console.log("update slot..", this.toJSON()); + puerts.merge(this.nativeSlotPtr, newProp); + UE.UMGManager.SynchronizeSlotProperties(this.nativeSlotPtr) + } else if (typeof newProp === 'function') { + this.unbind(key); + this.bind(key, newProp); + } else { + myProps[key] = newProp; + propChange = true; + } + } + } + if (propChange) { + //console.log("update props", this.toJSON(), JSON.stringify(myProps)); + puerts.merge(this.nativePtr, myProps); + UE.UMGManager.SynchronizeWidgetProperties(this.nativePtr) + } + } + + unbind(name: string) { + let remover = this.callbackRemovers[name]; + this.callbackRemovers[name] = undefined; + if (remover) { + remover(); + } + } + + unbindAll() { + for(var key in this.callbackRemovers) { + this.callbackRemovers[key](); + } + this.callbackRemovers = {}; + } + + appendChild(child: UEWidget) { + let nativeSlot = (this.nativePtr as UE.PanelWidget).AddChild(child.nativePtr); + //console.log("appendChild", (await this.nativePtr).toJSON(), (await child.nativePtr).toJSON()); + child.nativeSlot = nativeSlot; + } + + removeChild(child: UEWidget) { + child.unbindAll(); + (this.nativePtr as UE.PanelWidget).RemoveChild(child.nativePtr); + //console.log("removeChild", (await this.nativePtr).toJSON(), (await child.nativePtr).toJSON()) + } + + set nativeSlot(value: UE.PanelSlot) { + this.nativeSlotPtr = value; + //console.log('setting nativeSlot', value.toJSON()); + if (this.slot) { + puerts.merge(this.nativeSlotPtr, this.slot); + UE.UMGManager.SynchronizeSlotProperties(this.nativeSlotPtr); + } + } +} + +class UEWidgetRoot { + nativePtr: UE.ReactWidget; + Added: boolean; + + constructor(nativePtr: UE.ReactWidget) { + this.nativePtr = nativePtr; + } + + appendChild(child: UEWidget) { + let nativeSlot = this.nativePtr.AddChild(child.nativePtr); + child.nativeSlot = nativeSlot; + } + + removeChild(child: UEWidget) { + child.unbindAll(); + this.nativePtr.RemoveChild(child.nativePtr); + } + + addToViewport(z : number) { + if (!this.Added) { + this.nativePtr.AddToViewport(z); + this.Added = true; + } + } + + removeFromViewport() { + this.nativePtr.RemoveFromViewport(); + } + + getWidget() { + return this.nativePtr; + } +} + +const hostConfig : Reconciler.HostConfig = { + getRootHostContext () { + return {}; + }, + //CanvasPanel()的parentHostContext是getRootHostContext返回的值 + getChildHostContext (parentHostContext: {}) { + return parentHostContext;//no use, share one + }, + appendInitialChild (parent: UEWidget, child: UEWidget) { + parent.appendChild(child); + }, + appendChildToContainer (container: UEWidgetRoot, child: UEWidget) { + container.appendChild(child); + }, + appendChild (parent: UEWidget, child: UEWidget) { + parent.appendChild(child); + }, + createInstance (type: string, props: any) { + return new UEWidget(type, props); + }, + createTextInstance (text: string) { + return new UEWidget("TextBlock", {Text: text}); + }, + finalizeInitialChildren () { + return false + }, + getPublicInstance (instance: UEWidget) { + console.warn('getPublicInstance'); + return instance + }, + now: Date.now, + prepareForCommit () { + //log('prepareForCommit'); + }, + resetAfterCommit (container: UEWidgetRoot) { + container.addToViewport(0); + }, + resetTextContent () { + console.error('resetTextContent not implemented!'); + }, + shouldSetTextContent (type, props) { + return false + }, + + commitTextUpdate (textInstance: UEWidget, oldText: string, newText: string) { + if (oldText != newText) { + textInstance.update({}, {Text: newText}) + } + }, + + //return false表示不更新,真值将会出现到commitUpdate的updatePayload里头 + prepareUpdate (instance: UEWidget, type: string, oldProps: any, newProps: any) { + try{ + return !deepEquals(oldProps, newProps); + } catch(e) { + console.error(e.message); + return true; + } + }, + commitUpdate (instance: UEWidget, updatePayload: any, type : string, oldProps : any, newProps: any) { + try{ + instance.update(oldProps, newProps); + } catch(e) { + console.error("commitUpdate fail!, " + e); + } + }, + removeChildFromContainer (container: UEWidgetRoot, child: UEWidget) { + console.error('removeChildFromContainer'); + //container.removeChild(child).catch(e => { + // console.error('removeChildFromContainer , e:' + e.message); + //}); + }, + removeChild(parent: UEWidget, child: UEWidget) { + parent.removeChild(child); + }, + + //useSyncScheduling: true, + supportsMutation: true, + isPrimaryRenderer: true, + supportsPersistence: false, + supportsHydration: false, + + shouldDeprioritizeSubtree: undefined, + setTimeout: undefined, + clearTimeout: undefined, + cancelDeferredCallback: undefined, + noTimeout: undefined, + scheduleDeferredCallback: undefined, +} + +const reconciler = Reconciler(hostConfig) + +export const ReactUMG = { + render: function(reactElement: React.ReactNode) { + if (world == undefined) { + throw new Error("init with World first!"); + } + let root = new UEWidgetRoot(UE.UMGManager.CreateReactWidget(world)); + const container = reconciler.createContainer(root, false, false); + reconciler.updateContainer(reactElement, container, null, null); + return root; + }, + init: function(inWorld: UE.World) { + world = inWorld; + } +} + diff --git a/TypeScript/ui/index.tsx b/TypeScript/ui/index.tsx new file mode 100644 index 0000000..97da938 --- /dev/null +++ b/TypeScript/ui/index.tsx @@ -0,0 +1,52 @@ +import * as React from 'react'; +import { VerticalBox, CanvasPanel, ReactUMG, CanvasPanelSlot, Button, HorizontalBox/*, TextureImage*/ } from 'react-umg'; +import {StatusBar} from './ui-components' +interface Props { + names: string[]; +} + +interface State { + names: string[]; + buttonTextureIndex: 0 | 1; +} + +let SlotOfVerticalBox: CanvasPanelSlot = { + LayoutData: { + Offsets: { + Left: 120, + Top: 120, + Right: 480, + Bottom: 80 + } + } +} + +class Hello extends React.Component { + constructor(props: Props) { + super(props); + this.state = { + names: props.names, + buttonTextureIndex : 0, + }; + } + render() { + return ( + + + + + + {this.state.names.map((name, idx) => )} + + + ); + } +} + +export function Load(){ + return ReactUMG.render( + + ); +}; diff --git a/TypeScript/ui/ui-components.tsx b/TypeScript/ui/ui-components.tsx new file mode 100644 index 0000000..f0251b2 --- /dev/null +++ b/TypeScript/ui/ui-components.tsx @@ -0,0 +1,50 @@ +import * as React from 'react'; +import { Button, HorizontalBox, TextBlock, ProgressBar, HorizontalBoxSlot } from 'react-umg'; +import {LinearColor} from 'ue' + +export interface Props { + name: string; + initialPercent?: number; +} + +interface State { + percent: number; +} + +let SlotOfProgressBar: HorizontalBoxSlot = { + Size: { + SizeRule: 1 + } +} + +export class StatusBar extends React.Component { + constructor(props: Props) { + super(props); + + if ((props.initialPercent || 0) < 0) { + throw new Error('initialPercent < 0'); + } + + this.state = { + percent: props.initialPercent || 0.5 + }; + } + + get color(): Partial { + return {R: 1 - this.state.percent , G: 0, B: this.state.percent}; + } + + onIncrement = () => this.setState({percent: this.state.percent + 0.01}); + onDecrement = () => this.setState({percent: this.state.percent - 0.01}); + + render() { + return ( + + + + + + + ); + } +} \ No newline at end of file