Spaces:
Running
Running
Deploy browser app plus Blender and Unreal handoff
Browse files- .gitattributes +6 -0
- README.md +2 -2
- native/README.md +4 -0
- native/art/blender/__pycache__/generate_singularity_assets.cpython-314.pyc +0 -0
- native/art/blender/build_realistic_assets.py +277 -0
- native/art/blender/generate_singularity_assets.py +5 -0
- native/art/blender/verify_assets.py +23 -0
- native/art/generated/asset-manifest.json +64 -0
- native/art/generated/body-preview.png +3 -0
- native/art/generated/cargo_and_finish.glb +3 -0
- native/art/generated/course-preview.png +3 -0
- native/art/generated/course_modules.glb +3 -0
- native/art/generated/singularity-art.blend +3 -0
- native/art/generated/singularity_body.glb +3 -0
- native/singularity-native-handoff.zip +3 -0
- native/unreal/Config/DefaultEngine.ini +8 -0
- native/unreal/Config/DefaultInput.ini +6 -0
- native/unreal/README.md +32 -0
- native/unreal/Singularity.uproject +8 -0
- native/unreal/Source/Singularity.Target.cs +13 -0
- native/unreal/Source/Singularity/Singularity.Build.cs +9 -0
- native/unreal/Source/Singularity/Singularity.cpp +2 -0
- native/unreal/Source/Singularity/SingularityGameMode.cpp +19 -0
- native/unreal/Source/Singularity/SingularityGameMode.h +25 -0
- native/unreal/Source/Singularity/SingularityRolePlayerController.cpp +69 -0
- native/unreal/Source/Singularity/SingularityRolePlayerController.h +40 -0
- native/unreal/Source/Singularity/SingularityRoleTypes.h +65 -0
- native/unreal/Source/Singularity/SingularitySharedBody.cpp +191 -0
- native/unreal/Source/Singularity/SingularitySharedBody.h +69 -0
- native/unreal/Source/Singularity/SingularitySpacetimeBridge.cpp +41 -0
- native/unreal/Source/Singularity/SingularitySpacetimeBridge.h +38 -0
- native/unreal/Source/SingularityEditor.Target.cs +13 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,9 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
native/art/generated/body-preview.png filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
native/art/generated/cargo_and_finish.glb filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
native/art/generated/course-preview.png filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
native/art/generated/course_modules.glb filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
native/art/generated/singularity-art.blend filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
native/art/generated/singularity_body.glb filter=lfs diff=lfs merge=lfs -text
|
README.md
CHANGED
|
@@ -38,7 +38,7 @@ This initial competitive course uses a custom constrained-body solver and proced
|
|
| 38 |
|
| 39 |
## Unreal and Blender handoff
|
| 40 |
|
| 41 |
-
The Blender artwork is available in [`art/generated/singularity-art.blend`](art/generated/singularity-art.blend), with three exported GLBs and inspected Cycles previews. The industrial robot has six independent parts, painted metal, rubber joints, optical lenses, and embedded PBR textures. The course includes detailed decking, bridge, cargo, and finish assets. See [`unreal/README.md`](unreal/README.md) for regeneration and import instructions. The Unreal source includes corrected coordinate mapping and input handling, but remains uncompiled here; its SpacetimeDB bridge is an event interface and still needs a network transport. No skeletal animation or native multiplayer is claimed.
|
| 42 |
|
| 43 |
## Develop and verify
|
| 44 |
|
|
@@ -71,5 +71,5 @@ npm run build
|
|
| 71 |
python scripts/deploy.py "Deploy verified game build"
|
| 72 |
```
|
| 73 |
|
| 74 |
-
The script uses existing Hugging Face credentials and uploads the static production build. SpacetimeDB Cloud provides persistence. No paid graphics server is required. An offline backend is visibly reported; no fake online players or local ranked results are substituted.
|
| 75 |
|
|
|
|
| 38 |
|
| 39 |
## Unreal and Blender handoff
|
| 40 |
|
| 41 |
+
The Blender artwork is available in [`art/generated/singularity-art.blend`](art/generated/singularity-art.blend), with three exported GLBs and inspected Cycles previews. The industrial robot has six independent parts, painted metal, rubber joints, optical lenses, and embedded PBR textures. The course includes detailed decking, bridge, cargo, and finish assets. See [`unreal/README.md`](unreal/README.md) for regeneration and import instructions. The Unreal source includes corrected coordinate mapping and input handling, but remains uncompiled here; its SpacetimeDB bridge is an event interface and still needs a network transport. No skeletal animation or native multiplayer is claimed. The deployed Space includes the complete native handoff under [`native/`](native/).
|
| 42 |
|
| 43 |
## Develop and verify
|
| 44 |
|
|
|
|
| 71 |
python scripts/deploy.py "Deploy verified game build"
|
| 72 |
```
|
| 73 |
|
| 74 |
+
The script uses existing Hugging Face credentials and uploads the static production build plus `native/` containing the Blender exports/source and Unreal project. The default target is `sankalphs/singularity1`; pass a second argument to target another Space. SpacetimeDB Cloud provides persistence. No paid graphics server is required. An offline backend is visibly reported; no fake online players or local ranked results are substituted.
|
| 75 |
|
native/README.md
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Singularity native handoff
|
| 2 |
+
|
| 3 |
+
This folder contains the Blender source/export and Unreal Engine 5.4 project.
|
| 4 |
+
Run the Blender scripts first, then open `unreal/Singularity.uproject` in Unreal.
|
native/art/blender/__pycache__/generate_singularity_assets.cpython-314.pyc
ADDED
|
Binary file (10 kB). View file
|
|
|
native/art/blender/build_realistic_assets.py
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Blender-only industrial art build, GLB exports, packed .blend and review renders.
|
| 2 |
+
Run: blender --background --python art/blender/build_realistic_assets.py
|
| 3 |
+
Blender coordinates: X lateral, Y course-forward, Z up, metres.
|
| 4 |
+
"""
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
import math
|
| 7 |
+
import json
|
| 8 |
+
import bpy
|
| 9 |
+
import numpy as np
|
| 10 |
+
from mathutils import Vector
|
| 11 |
+
|
| 12 |
+
OUT = Path(__file__).resolve().parents[1] / 'generated'
|
| 13 |
+
OUT.mkdir(parents=True, exist_ok=True)
|
| 14 |
+
bpy.ops.object.select_all(action='SELECT')
|
| 15 |
+
bpy.ops.object.delete(use_global=False)
|
| 16 |
+
scene = bpy.context.scene
|
| 17 |
+
scene.unit_settings.system = 'METRIC'
|
| 18 |
+
scene.render.engine = 'CYCLES'
|
| 19 |
+
scene.cycles.samples = 32
|
| 20 |
+
scene.cycles.use_denoising = True
|
| 21 |
+
scene.render.resolution_percentage = 100
|
| 22 |
+
scene.world.color = (0.16, 0.16, 0.16)
|
| 23 |
+
scene.view_settings.view_transform = 'AgX'
|
| 24 |
+
rng = np.random.default_rng(19)
|
| 25 |
+
|
| 26 |
+
def texture(name, base, rough=False):
|
| 27 |
+
size = 256
|
| 28 |
+
noise = rng.random((size, size))
|
| 29 |
+
scratches = rng.random((size, 1)) > .97
|
| 30 |
+
pixels = np.ones((size, size, 4), dtype=np.float32)
|
| 31 |
+
for channel in range(3):
|
| 32 |
+
pixels[:, :, channel] = np.clip(base[channel] * (.88 + .18 * noise) + scratches * .025, 0, 1)
|
| 33 |
+
img = bpy.data.images.new(name, width=size, height=size)
|
| 34 |
+
if rough:
|
| 35 |
+
img.colorspace_settings.name = 'Non-Color'
|
| 36 |
+
img.pixels.foreach_set(pixels.ravel())
|
| 37 |
+
img.pack()
|
| 38 |
+
return img
|
| 39 |
+
|
| 40 |
+
def material(name, color, metal=0, roughness=.45, textured=False):
|
| 41 |
+
mat = bpy.data.materials.new(name)
|
| 42 |
+
mat.use_nodes = True
|
| 43 |
+
mat.diffuse_color = (*color, 1)
|
| 44 |
+
shader = mat.node_tree.nodes.get('Principled BSDF')
|
| 45 |
+
shader.inputs['Base Color'].default_value = (*color, 1)
|
| 46 |
+
shader.inputs['Metallic'].default_value = metal
|
| 47 |
+
shader.inputs['Roughness'].default_value = roughness
|
| 48 |
+
if textured:
|
| 49 |
+
for socket, image in [('Base Color', texture(name+'_color', color)),
|
| 50 |
+
('Roughness', texture(name+'_roughness', (roughness,)*3, True))]:
|
| 51 |
+
node = mat.node_tree.nodes.new('ShaderNodeTexImage')
|
| 52 |
+
node.image = image
|
| 53 |
+
mat.node_tree.links.new(node.outputs['Color'], shader.inputs[socket])
|
| 54 |
+
return mat
|
| 55 |
+
|
| 56 |
+
coral = material('TeamCoral', (.48, .065, .025), .65, .32, True)
|
| 57 |
+
mint = material('TeamMint', (.055, .38, .28), .6, .36, True)
|
| 58 |
+
steel = material('BrushedTitanium', (.31, .36, .40), .85, .3, True)
|
| 59 |
+
dark = material('GraphiteCeramic', (.028, .04, .052), .65, .34, True)
|
| 60 |
+
rubber = material('JointRubber', (.012, .016, .018), .05, .78, True)
|
| 61 |
+
deck = material('WeatheredDeck', (.25, .29, .3), .7, .57, True)
|
| 62 |
+
gold = material('SafetyOchre', (.65, .34, .035), .5, .42, True)
|
| 63 |
+
glass = material('OpticalGlass', (.012, .052, .068), .8, .12)
|
| 64 |
+
white = material('MarkingIvory', (.78, .81, .75), .05, .42)
|
| 65 |
+
lightmat = material('StatusLED', (.09, .7, .51), .3, .2)
|
| 66 |
+
bsdf = lightmat.node_tree.nodes.get('Principled BSDF')
|
| 67 |
+
bsdf.inputs['Emission Color'].default_value = (.1, 1, .66, 1)
|
| 68 |
+
bsdf.inputs['Emission Strength'].default_value = 3
|
| 69 |
+
|
| 70 |
+
def box(name, pos, size, mat, bevel=.025):
|
| 71 |
+
bpy.ops.mesh.primitive_cube_add(size=1, location=pos)
|
| 72 |
+
obj = bpy.context.object
|
| 73 |
+
obj.name = name
|
| 74 |
+
obj.scale = size
|
| 75 |
+
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
|
| 76 |
+
obj.data.materials.append(mat)
|
| 77 |
+
if bevel:
|
| 78 |
+
mod = obj.modifiers.new('Machined edge radius', 'BEVEL')
|
| 79 |
+
mod.width = bevel
|
| 80 |
+
mod.segments = 3
|
| 81 |
+
mod = obj.modifiers.new('Weighted corner normals', 'WEIGHTED_NORMAL')
|
| 82 |
+
return obj
|
| 83 |
+
|
| 84 |
+
def ball(name, pos, size, mat):
|
| 85 |
+
bpy.ops.mesh.primitive_uv_sphere_add(segments=24, ring_count=12, location=pos)
|
| 86 |
+
obj = bpy.context.object
|
| 87 |
+
obj.name = name
|
| 88 |
+
obj.scale = size
|
| 89 |
+
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
|
| 90 |
+
obj.data.materials.append(mat)
|
| 91 |
+
for poly in obj.data.polygons:
|
| 92 |
+
poly.use_smooth = True
|
| 93 |
+
return obj
|
| 94 |
+
|
| 95 |
+
def rod(name, a, b, radius, mat, vertices=16):
|
| 96 |
+
a, b = Vector(a), Vector(b)
|
| 97 |
+
bpy.ops.mesh.primitive_cylinder_add(vertices=vertices, radius=radius, depth=(b-a).length, location=(a+b)/2)
|
| 98 |
+
obj = bpy.context.object
|
| 99 |
+
obj.name = name
|
| 100 |
+
obj.rotation_mode = 'QUATERNION'
|
| 101 |
+
obj.rotation_quaternion = (b-a).to_track_quat('Z', 'Y')
|
| 102 |
+
obj.data.materials.append(mat)
|
| 103 |
+
bevel = obj.modifiers.new('Turned edge', 'BEVEL')
|
| 104 |
+
bevel.width = min(.008, radius*.2)
|
| 105 |
+
bevel.segments = 2
|
| 106 |
+
return obj
|
| 107 |
+
|
| 108 |
+
def label(text, pos, size, mat=white):
|
| 109 |
+
bpy.ops.object.text_add(location=pos, rotation=(math.pi/2, 0, math.pi))
|
| 110 |
+
obj = bpy.context.object
|
| 111 |
+
obj.data.body = text
|
| 112 |
+
obj.data.align_x = 'CENTER'
|
| 113 |
+
obj.data.size = size
|
| 114 |
+
obj.data.extrude = .001
|
| 115 |
+
obj.data.materials.append(mat)
|
| 116 |
+
bpy.ops.object.convert(target='MESH')
|
| 117 |
+
return bpy.context.object
|
| 118 |
+
|
| 119 |
+
def group_since(before, name):
|
| 120 |
+
objects = [o for o in scene.objects if o not in before and o.type == 'MESH']
|
| 121 |
+
collection = bpy.data.collections.new(name)
|
| 122 |
+
scene.collection.children.link(collection)
|
| 123 |
+
for obj in objects:
|
| 124 |
+
for old in list(obj.users_collection):
|
| 125 |
+
old.objects.unlink(obj)
|
| 126 |
+
collection.objects.link(obj)
|
| 127 |
+
return objects
|
| 128 |
+
|
| 129 |
+
def export(name, objects):
|
| 130 |
+
bpy.ops.object.select_all(action='DESELECT')
|
| 131 |
+
for obj in objects:
|
| 132 |
+
obj.select_set(True)
|
| 133 |
+
bpy.context.view_layer.objects.active = objects[0]
|
| 134 |
+
bpy.ops.export_scene.gltf(filepath=str(OUT/name), export_format='GLB', use_selection=True,
|
| 135 |
+
export_apply=True, export_animations=False, export_extras=True)
|
| 136 |
+
|
| 137 |
+
body_start = set(scene.objects)
|
| 138 |
+
roles = [('EyesHead',(0,0,3)), ('LeftArm',(-1,0,2)), ('RightArm',(1,0,2)),
|
| 139 |
+
('TorsoBack',(0,0,2)), ('LeftLeg',(-.45,0,.35)), ('RightLeg',(.45,0,.35))]
|
| 140 |
+
for role, center in roles:
|
| 141 |
+
before = set(scene.objects)
|
| 142 |
+
x,y,z = center
|
| 143 |
+
if role == 'EyesHead':
|
| 144 |
+
ball('Helmet shell', center, (.4,.32,.42), coral)
|
| 145 |
+
box('Visor surround', (0,.27,3.02), (.66,.14,.23), dark, .07)
|
| 146 |
+
for side in [-1,1]:
|
| 147 |
+
rod('Optical housing',(side*.18,.32,3.02),(side*.18,.40,3.02),.096,steel,32)
|
| 148 |
+
rod('Optical lens',(side*.18,.401,3.02),(side*.18,.413,3.02),.074,glass,32)
|
| 149 |
+
ball('Sensor LED',(side*.27,.35,2.98),(.017,.015,.017),lightmat)
|
| 150 |
+
box('Brow plate',(0,.24,3.27),(.49,.18,.065),steel)
|
| 151 |
+
elif role == 'TorsoBack':
|
| 152 |
+
box('Pressure hull',center,(.82,.54,.94),dark,.13)
|
| 153 |
+
box('Chest armour',(0,.29,2.08),(.71,.14,.68),coral,.075)
|
| 154 |
+
label('S / 06',(0,.37,2.07),.13)
|
| 155 |
+
for i in range(5):
|
| 156 |
+
box('Intake louvre',(-.21+i*.105,.375,1.86),(.057,.035,.10),rubber,.008)
|
| 157 |
+
for side in [-1,1]:
|
| 158 |
+
rod('Back power canister',(side*.25,-.3,1.68),(side*.25,-.3,2.35),.105,steel)
|
| 159 |
+
rod('Shoulder coupler',(side*.38,0,2.2),(side*.75,0,2.1),.105,rubber)
|
| 160 |
+
rod('Hip actuator',(side*.22,0,1.62),(side*.42,0,.67),.09,steel)
|
| 161 |
+
rod('Neck',(0,0,2.43),(0,0,2.72),.13,rubber)
|
| 162 |
+
elif 'Arm' in role:
|
| 163 |
+
box('Forearm housing',center,(.29,.33,.54),coral,.075)
|
| 164 |
+
ball('Universal wrist',(x,0,1.68),(.15,.16,.13),rubber)
|
| 165 |
+
box('Palm',(x,.05,1.49),(.25,.19,.23),dark,.045)
|
| 166 |
+
for finger in [-1,0,1]:
|
| 167 |
+
box('Gripper finger',(x+finger*.085,.12,1.30),(.065,.15,.19),steel,.025)
|
| 168 |
+
for i in range(4):
|
| 169 |
+
box('Cooling rib',(x,0,2.17+i*.034),(.30,.34,.013),rubber,.004)
|
| 170 |
+
else:
|
| 171 |
+
box('Ankle armour',(x,0,.5),(.29,.34,.37),coral,.05)
|
| 172 |
+
box('Boot',(x,.13,.22),(.38,.64,.27),dark,.065)
|
| 173 |
+
box('Toe guard',(x,.37,.26),(.35,.15,.16),steel,.03)
|
| 174 |
+
for i in range(6):
|
| 175 |
+
box('Sole tread',(x,-.12+i*.09,.075),(.39,.048,.055),rubber,.008)
|
| 176 |
+
for side in [-1,1]:
|
| 177 |
+
rod('Hex fastener',(x+side*.10,.18,z+.12),(x+side*.10,.20,z+.12),.027,steel,6)
|
| 178 |
+
objects = [o for o in scene.objects if o not in before]
|
| 179 |
+
# Each exported role is one multi-material mesh with its pivot at the solver node.
|
| 180 |
+
bpy.ops.object.select_all(action='DESELECT')
|
| 181 |
+
for obj in objects:
|
| 182 |
+
obj.select_set(True)
|
| 183 |
+
bpy.context.view_layer.objects.active=obj
|
| 184 |
+
bpy.ops.object.convert(target='MESH')
|
| 185 |
+
bpy.context.view_layer.objects.active=objects[0]
|
| 186 |
+
bpy.ops.object.join()
|
| 187 |
+
obj=bpy.context.object
|
| 188 |
+
obj.name=role
|
| 189 |
+
scene.cursor.location=center
|
| 190 |
+
bpy.ops.object.origin_set(type='ORIGIN_CURSOR')
|
| 191 |
+
obj['singularity_role']=role
|
| 192 |
+
body = group_since(body_start,'SingularityBody')
|
| 193 |
+
export('singularity_body.glb',body)
|
| 194 |
+
|
| 195 |
+
before=set(scene.objects)
|
| 196 |
+
for start,end in [(-5,7),(15,27),(27,47)]:
|
| 197 |
+
box('Structural platform',(0,(start+end)/2,-.34),(9.2,end-start,.64),dark,.09)
|
| 198 |
+
for yy in np.arange(start+.5,end,1):
|
| 199 |
+
for xx in [-3.45,-1.15,1.15,3.45]:
|
| 200 |
+
box('Deck panel',(xx,float(yy),.005),(2.25,.95,.10),deck,.018)
|
| 201 |
+
for dx in [-1,1]:
|
| 202 |
+
rod('Deck rivet',(xx+dx, float(yy)-.32,.056),(xx+dx,float(yy)-.32,.064),.035,steel,6)
|
| 203 |
+
for xx in [-4.5,4.5]:
|
| 204 |
+
box('Edge beacon',(xx,float(yy),.095),(.045,.52,.025),lightmat,.005)
|
| 205 |
+
for xx in [-4.65,4.65]:
|
| 206 |
+
box('Perimeter beam',(xx,(start+end)/2,-.22),(.17,end-start,.22),steel)
|
| 207 |
+
for yy in np.arange(7.15,15,.30):
|
| 208 |
+
box('Bridge plank',(0,float(yy),0),(2.58,.27,.12),steel,.018)
|
| 209 |
+
for xx in [-1.15,1.15]:
|
| 210 |
+
rod('Bridge cable',(xx,7,-.1),(xx,15,-.1),.045,dark)
|
| 211 |
+
for yy in [6.4,15.5,27.5,41.4]:
|
| 212 |
+
for xx in np.arange(-4.2,4.3,.6):
|
| 213 |
+
box('Hazard stripe',(float(xx),yy,.071),(.30,.35,.008),gold,.002)
|
| 214 |
+
box('Delivery inset',(0,26,.07),(4.8,2.6,.025),mint)
|
| 215 |
+
delivery_label=label('02 / CARGO',(0,24.6,.095),.32)
|
| 216 |
+
delivery_label.rotation_euler=(0,0,math.pi)
|
| 217 |
+
course=group_since(before,'CourseModules')
|
| 218 |
+
export('course_modules.glb',course)
|
| 219 |
+
|
| 220 |
+
before=set(scene.objects)
|
| 221 |
+
box('Cargo shell',(0,18,.54),(.9,.9,.9),gold,.065)
|
| 222 |
+
for x in [-.43,.43]:
|
| 223 |
+
for y in [17.57,18.43]:
|
| 224 |
+
box('Cargo bumper',(x,y,.54),(.13,.13,.98),rubber,.025)
|
| 225 |
+
for z in [.14,.90]:
|
| 226 |
+
box('Cargo band',(0,18,z),(.94,.94,.07),steel)
|
| 227 |
+
label('CARGO',(0,18.456,.58),.13,dark)
|
| 228 |
+
for x in [-3.8,3.8]:
|
| 229 |
+
box('Finish pillar',(x,42,2.8),(.32,.5,5.6),dark,.055)
|
| 230 |
+
box('Finish light',(x,42.27,2.8),(.07,.035,5.2),lightmat,.012)
|
| 231 |
+
box('Finish crossbeam',(0,42,5.6),(8.2,.5,.6),steel,.07)
|
| 232 |
+
label('S I N G U L A R I T Y',(0,42.26,5.51),.26,dark)
|
| 233 |
+
rod('Sweeper mast',(0,34,.1),(0,34,2.8),.11,dark)
|
| 234 |
+
box('Sweeper arm',(0,34,1.5),(7.2,.24,.27),gold,.06)
|
| 235 |
+
finish=group_since(before,'CargoAndFinish')
|
| 236 |
+
export('cargo_and_finish.glb',finish)
|
| 237 |
+
|
| 238 |
+
def area(name,pos,power,size,color):
|
| 239 |
+
bpy.ops.object.light_add(type='AREA',location=pos)
|
| 240 |
+
obj=bpy.context.object
|
| 241 |
+
obj.name=name
|
| 242 |
+
obj.data.energy=power
|
| 243 |
+
obj.data.shape='DISK'
|
| 244 |
+
obj.data.size=size
|
| 245 |
+
obj.data.color=color
|
| 246 |
+
obj.rotation_euler=(Vector((0,0,1.6))-obj.location).to_track_quat('-Z','Y').to_euler()
|
| 247 |
+
area('Warm key',(3,4,8),1800,5,(1,.83,.67))
|
| 248 |
+
area('Cool rim',(-4,-3,6),2200,4,(.57,.75,1))
|
| 249 |
+
area('Soft fill',(-3,4,3),900,4,(.8,.93,1))
|
| 250 |
+
bpy.ops.object.light_add(type='SUN',rotation=(.4,-.5,-.5))
|
| 251 |
+
bpy.context.object.data.energy=2
|
| 252 |
+
bpy.context.object.data.angle=.15
|
| 253 |
+
bpy.ops.object.camera_add()
|
| 254 |
+
camera=bpy.context.object
|
| 255 |
+
scene.camera=camera
|
| 256 |
+
def shot(name,pos,target,lens,width,height):
|
| 257 |
+
camera.location=pos
|
| 258 |
+
camera.rotation_euler=(Vector(target)-camera.location).to_track_quat('-Z','Y').to_euler()
|
| 259 |
+
camera.data.lens=lens
|
| 260 |
+
scene.render.resolution_x=width
|
| 261 |
+
scene.render.resolution_y=height
|
| 262 |
+
scene.render.filepath=str(OUT/name)
|
| 263 |
+
bpy.ops.render.render(write_still=True)
|
| 264 |
+
|
| 265 |
+
manifest={'units':'metres','axes':'Blender X lateral, Y forward, Z up',
|
| 266 |
+
'roles':[r for r,_ in roles], 'role_pivots':dict(roles),
|
| 267 |
+
'exports':['singularity_body.glb','course_modules.glb','cargo_and_finish.glb'],
|
| 268 |
+
'materials':[m.name for m in bpy.data.materials],
|
| 269 |
+
'mesh_objects':len([o for o in scene.objects if o.type=='MESH'])}
|
| 270 |
+
(OUT/'asset-manifest.json').write_text(json.dumps(manifest,indent=2))
|
| 271 |
+
shot('body-preview.png',(4.5,7,3.6),(0,0,1.65),58,1100,1100)
|
| 272 |
+
camera.data.type='ORTHO'
|
| 273 |
+
camera.data.ortho_scale=64
|
| 274 |
+
shot('course-preview.png',(36,60,35),(0,21,0),46,1600,1000)
|
| 275 |
+
bpy.context.preferences.filepaths.save_version=0
|
| 276 |
+
bpy.ops.wm.save_as_mainfile(filepath=str(OUT/'singularity-art.blend'))
|
| 277 |
+
print('ASSET_BUILD_COMPLETE',OUT)
|
native/art/blender/generate_singularity_assets.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Compatibility entry point for the Blender industrial asset builder."""
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
import runpy
|
| 4 |
+
|
| 5 |
+
runpy.run_path(str(Path(__file__).with_name("build_realistic_assets.py")), run_name="__main__")
|
native/art/blender/verify_assets.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Round-trip generated GLBs through Blender, checking real geometry and textures."""
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
import bpy
|
| 4 |
+
|
| 5 |
+
root=Path(__file__).resolve().parents[1]/'generated'
|
| 6 |
+
for name in ['singularity_body.glb','course_modules.glb','cargo_and_finish.glb']:
|
| 7 |
+
bpy.ops.object.select_all(action='SELECT')
|
| 8 |
+
bpy.ops.object.delete(use_global=False)
|
| 9 |
+
bpy.ops.import_scene.gltf(filepath=str(root/name))
|
| 10 |
+
meshes=[o for o in bpy.context.scene.objects if o.type=='MESH']
|
| 11 |
+
assert meshes, name
|
| 12 |
+
for obj in meshes:
|
| 13 |
+
assert len(obj.data.polygons)>0, obj.name
|
| 14 |
+
assert obj.data.uv_layers, obj.name
|
| 15 |
+
if name=='singularity_body.glb':
|
| 16 |
+
assert {o.name for o in meshes}=={'EyesHead','LeftArm','RightArm','TorsoBack','LeftLeg','RightLeg'}
|
| 17 |
+
for obj in meshes:
|
| 18 |
+
assert len(obj.data.materials)>=4, obj.name
|
| 19 |
+
textures=[n.image for obj in meshes for mat in obj.data.materials if mat and mat.use_nodes
|
| 20 |
+
for n in mat.node_tree.nodes if n.type=='TEX_IMAGE' and n.image]
|
| 21 |
+
assert textures and all(img.size[0]>0 for img in textures), name
|
| 22 |
+
print('ROUND_TRIP_OK',name,len(meshes),'meshes',len(textures),'texture references')
|
| 23 |
+
print('ALL_ASSETS_VERIFIED')
|
native/art/generated/asset-manifest.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"units": "metres",
|
| 3 |
+
"axes": "Blender X lateral, Y forward, Z up",
|
| 4 |
+
"roles": [
|
| 5 |
+
"EyesHead",
|
| 6 |
+
"LeftArm",
|
| 7 |
+
"RightArm",
|
| 8 |
+
"TorsoBack",
|
| 9 |
+
"LeftLeg",
|
| 10 |
+
"RightLeg"
|
| 11 |
+
],
|
| 12 |
+
"role_pivots": {
|
| 13 |
+
"EyesHead": [
|
| 14 |
+
0,
|
| 15 |
+
0,
|
| 16 |
+
3
|
| 17 |
+
],
|
| 18 |
+
"LeftArm": [
|
| 19 |
+
-1,
|
| 20 |
+
0,
|
| 21 |
+
2
|
| 22 |
+
],
|
| 23 |
+
"RightArm": [
|
| 24 |
+
1,
|
| 25 |
+
0,
|
| 26 |
+
2
|
| 27 |
+
],
|
| 28 |
+
"TorsoBack": [
|
| 29 |
+
0,
|
| 30 |
+
0,
|
| 31 |
+
2
|
| 32 |
+
],
|
| 33 |
+
"LeftLeg": [
|
| 34 |
+
-0.45,
|
| 35 |
+
0,
|
| 36 |
+
0.35
|
| 37 |
+
],
|
| 38 |
+
"RightLeg": [
|
| 39 |
+
0.45,
|
| 40 |
+
0,
|
| 41 |
+
0.35
|
| 42 |
+
]
|
| 43 |
+
},
|
| 44 |
+
"exports": [
|
| 45 |
+
"singularity_body.glb",
|
| 46 |
+
"course_modules.glb",
|
| 47 |
+
"cargo_and_finish.glb"
|
| 48 |
+
],
|
| 49 |
+
"materials": [
|
| 50 |
+
"BrushedTitanium",
|
| 51 |
+
"Dots Stroke",
|
| 52 |
+
"GraphiteCeramic",
|
| 53 |
+
"JointRubber",
|
| 54 |
+
"MarkingIvory",
|
| 55 |
+
"Material",
|
| 56 |
+
"OpticalGlass",
|
| 57 |
+
"SafetyOchre",
|
| 58 |
+
"StatusLED",
|
| 59 |
+
"TeamCoral",
|
| 60 |
+
"TeamMint",
|
| 61 |
+
"WeatheredDeck"
|
| 62 |
+
],
|
| 63 |
+
"mesh_objects": 738
|
| 64 |
+
}
|
native/art/generated/body-preview.png
ADDED
|
Git LFS Details
|
native/art/generated/cargo_and_finish.glb
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e7a70da39aff7cde9b8a050c74661e547f852973cec4fcc7c6e60670b932fbcf
|
| 3 |
+
size 839048
|
native/art/generated/course-preview.png
ADDED
|
Git LFS Details
|
native/art/generated/course_modules.glb
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d01d13cf500accf3cfa7c5ab855b15b691502d81e41ffdbb81ee1394a5642559
|
| 3 |
+
size 8064152
|
native/art/generated/singularity-art.blend
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:2703c45ff18cf877bcebe347737885d119eb87577646b70358eb502ae363df39
|
| 3 |
+
size 7799645
|
native/art/generated/singularity_body.glb
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:cb8d625563924d2d2fb47854f2775a3aa723fca89fcb3ec6264ddea08eb05fa7
|
| 3 |
+
size 1449444
|
native/singularity-native-handoff.zip
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:fb2cb0023cd889108621fd5c3166eb7cd5a495a624f75503868e668e45d7a7e9
|
| 3 |
+
size 7423317
|
native/unreal/Config/DefaultEngine.ini
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[/Script/EngineSettings.GameMapsSettings]
|
| 2 |
+
GameDefaultMap=/Game/Singularity/Maps/Lobby
|
| 3 |
+
EditorStartupMap=/Game/Singularity/Maps/Lobby
|
| 4 |
+
GlobalDefaultGameMode=/Script/Singularity.SingularityGameMode
|
| 5 |
+
|
| 6 |
+
[/Script/OnlineSubsystemUtils.IpNetDriver]
|
| 7 |
+
NetServerMaxTickRate=60
|
| 8 |
+
MaxClientRate=100000
|
native/unreal/Config/DefaultInput.ini
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[/Script/Engine.InputSettings]
|
| 2 |
+
+AxisMappings=(AxisName="Singularity_MoveX",Key=A,Scale=-1.0)
|
| 3 |
+
+AxisMappings=(AxisName="Singularity_MoveX",Key=D,Scale=1.0)
|
| 4 |
+
+AxisMappings=(AxisName="Singularity_MoveZ",Key=S,Scale=-1.0)
|
| 5 |
+
+AxisMappings=(AxisName="Singularity_MoveZ",Key=W,Scale=1.0)
|
| 6 |
+
+ActionMappings=(ActionName="Singularity_Action",Key=SpaceBar)
|
native/unreal/README.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Unreal / Blender handoff
|
| 2 |
+
|
| 3 |
+
The generated art is ready in `../art/generated/`: three GLBs with embedded PBR textures, a packed `singularity-art.blend`, and two Cycles preview renders. All art was built and rendered in Blender 4.5.9; no external asset downloads or AI images are used.
|
| 4 |
+
|
| 5 |
+
## Open the artwork
|
| 6 |
+
|
| 7 |
+
Open `../art/generated/singularity-art.blend` in Blender. Collections separate the six-part robot, deck/bridge, and cargo/finish/sweeper. The robot uses painted metal armour, machined fasteners, optical lenses, rubber joints, grippers, and treaded boots. Texture images are packed into the blend and embedded in each GLB. These are rigid meshes, not a skeletal character or animation montage.
|
| 8 |
+
|
| 9 |
+
Regenerate with:
|
| 10 |
+
```powershell
|
| 11 |
+
& '../.tools/blender-4.5.9-windows-x64/blender.exe' --background --python ../art/blender/generate_singularity_assets.py
|
| 12 |
+
& '../.tools/blender-4.5.9-windows-x64/blender.exe' --background --python ../art/blender/verify_assets.py
|
| 13 |
+
```
|
| 14 |
+
Commands above run from `unreal/`. Any Blender 4.5 installation may replace the portable executable path.
|
| 15 |
+
|
| 16 |
+
## Unreal setup (requires editor verification)
|
| 17 |
+
|
| 18 |
+
Build the supplied game/editor targets in Unreal 5.4, then create a level at `/Game/Singularity/Maps/Lobby`. That configured level is not yet supplied as a .umap. Import the three GLBs using scene import so course object transforms are retained. Keep body meshes separate, retain their local pivots, and enable material/texture import. Add simple collision for any meshes you wish to simulate.
|
| 19 |
+
|
| 20 |
+
Create a Blueprint subclass of `ASingularitySharedBody`. Assign `RoleMeshes` in order: EyesHead, LeftArm, RightArm, TorsoBack, LeftLeg, RightLeg. Meshes are local to their solver node; keep the component scale at one after the importer converts metres to centimetres. TeamMaterial overrides only named TeamCoral/TeamMint paint slots, preserving rubber, steel, and optics.
|
| 21 |
+
|
| 22 |
+
Server coordinates are metres: lateral X, up Y, forward Z. `ApplyAuthoritativeSnapshot` maps them to Unreal centimetres as `(Z, X, Y) * 100` and maps torso-first node order into role order. Ranked presentation is kinematic. Explicitly enabling Chaos on the parts is an experimental local preview; it is not the ranked simulation. Constraints have midpoint anchors and angular limits; local inputs expire after 500 ms and a hop requires a grounded press.
|
| 23 |
+
|
| 24 |
+
## Networking boundary
|
| 25 |
+
|
| 26 |
+
`USingularitySpacetimeBridge` is an event interface, not a connected SpacetimeDB client. A transport still needs authentication, subscriptions, JSON decoding, role leases, room/result handling, and reducer calls. The actual server join role is a numeric 0–5; input reducer payload is x/z/action. ClientTick and Role in the local packet are routing metadata, not additional input reducer arguments.
|
| 27 |
+
|
| 28 |
+
Bind the player's bridge OnInputReady to the transport's input reducer, and feed decoded team snapshots into OnSnapshotReady / ApplyAuthoritativeSnapshot. Do not bind local force inputs to the ranked body. The controller uses WASD/Space at up to 30 Hz through DefaultInput.ini. The GameMode creates its bridge before BeginPlay; remote native clients own a local controller bridge.
|
| 29 |
+
|
| 30 |
+
## Verification limits
|
| 31 |
+
|
| 32 |
+
The exported files pass binary GLB tests and a Blender import round trip. Preview renders were visually inspected. Unreal is not installed in this environment: C++ compilation, import orientation, collision, Blueprint wiring, and native multiplayer remain unverified. The web build does not validate Unreal code.
|
native/unreal/Singularity.uproject
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"FileVersion": 3,
|
| 3 |
+
"EngineAssociation": "5.4",
|
| 4 |
+
"Category": "Games",
|
| 5 |
+
"Description": "Singularity shared-body competitive physics game",
|
| 6 |
+
"Modules": [{"Name":"Singularity","Type":"Runtime","LoadingPhase":"Default"}],
|
| 7 |
+
"Plugins": [{"Name":"EnhancedInput","Enabled":true},{"Name":"OnlineSubsystem","Enabled":true}]
|
| 8 |
+
}
|
native/unreal/Source/Singularity.Target.cs
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
using UnrealBuildTool;
|
| 2 |
+
using System.Collections.Generic;
|
| 3 |
+
|
| 4 |
+
public class SingularityTarget : TargetRules
|
| 5 |
+
{
|
| 6 |
+
public SingularityTarget(TargetInfo Target) : base(Target)
|
| 7 |
+
{
|
| 8 |
+
Type = TargetType.Game;
|
| 9 |
+
DefaultBuildSettings = BuildSettingsVersion.V5;
|
| 10 |
+
IncludeOrderVersion = EngineIncludeOrderVersion.Unreal5_4;
|
| 11 |
+
ExtraModuleNames.Add("Singularity");
|
| 12 |
+
}
|
| 13 |
+
}
|
native/unreal/Source/Singularity/Singularity.Build.cs
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
using UnrealBuildTool;
|
| 2 |
+
public class Singularity : ModuleRules
|
| 3 |
+
{
|
| 4 |
+
public Singularity(ReadOnlyTargetRules Target) : base(Target)
|
| 5 |
+
{
|
| 6 |
+
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
|
| 7 |
+
PublicDependencyModuleNames.AddRange(new[] { "Core", "CoreUObject", "Engine", "PhysicsCore", "EnhancedInput", "OnlineSubsystemUtils" });
|
| 8 |
+
}
|
| 9 |
+
}
|
native/unreal/Source/Singularity/Singularity.cpp
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#include "Modules/ModuleManager.h"
|
| 2 |
+
IMPLEMENT_PRIMARY_GAME_MODULE(FDefaultGameModuleImpl, Singularity, "Singularity");
|
native/unreal/Source/Singularity/SingularityGameMode.cpp
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#include "SingularityGameMode.h"
|
| 2 |
+
#include "SingularitySharedBody.h"
|
| 3 |
+
#include "SingularitySpacetimeBridge.h"
|
| 4 |
+
#include "SingularityRolePlayerController.h"
|
| 5 |
+
|
| 6 |
+
ASingularityGameMode::ASingularityGameMode()
|
| 7 |
+
{
|
| 8 |
+
PrimaryActorTick.bCanEverTick = false;
|
| 9 |
+
SharedBodyClass = ASingularitySharedBody::StaticClass();
|
| 10 |
+
PlayerControllerClass = ASingularityRolePlayerController::StaticClass();
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
void ASingularityGameMode::StartPlay()
|
| 14 |
+
{
|
| 15 |
+
// The bridge is intentionally transport-agnostic. A Blueprint or an
|
| 16 |
+
// Unreal SpacetimeDB plugin binds its events to this single game object.
|
| 17 |
+
SpacetimeBridge = NewObject<USingularitySpacetimeBridge>(this);
|
| 18 |
+
Super::StartPlay();
|
| 19 |
+
}
|
native/unreal/Source/Singularity/SingularityGameMode.h
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#pragma once
|
| 2 |
+
#include "CoreMinimal.h"
|
| 3 |
+
#include "GameFramework/GameModeBase.h"
|
| 4 |
+
#include "SingularityRoleTypes.h"
|
| 5 |
+
#include "SingularityGameMode.generated.h"
|
| 6 |
+
|
| 7 |
+
class ASingularitySharedBody;
|
| 8 |
+
class USingularitySpacetimeBridge;
|
| 9 |
+
class ASingularityRolePlayerController;
|
| 10 |
+
|
| 11 |
+
UCLASS()
|
| 12 |
+
class SINGULARITY_API ASingularityGameMode : public AGameModeBase
|
| 13 |
+
{
|
| 14 |
+
GENERATED_BODY()
|
| 15 |
+
public:
|
| 16 |
+
ASingularityGameMode();
|
| 17 |
+
|
| 18 |
+
virtual void StartPlay() override;
|
| 19 |
+
|
| 20 |
+
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Singularity")
|
| 21 |
+
TSubclassOf<ASingularitySharedBody> SharedBodyClass;
|
| 22 |
+
|
| 23 |
+
UPROPERTY(BlueprintReadOnly, Category = "Singularity")
|
| 24 |
+
TObjectPtr<USingularitySpacetimeBridge> SpacetimeBridge;
|
| 25 |
+
};
|
native/unreal/Source/Singularity/SingularityRolePlayerController.cpp
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#include "SingularityRolePlayerController.h"
|
| 2 |
+
|
| 3 |
+
#include "Components/InputComponent.h"
|
| 4 |
+
#include "Engine/World.h"
|
| 5 |
+
#include "SingularityGameMode.h"
|
| 6 |
+
#include "SingularitySpacetimeBridge.h"
|
| 7 |
+
|
| 8 |
+
ASingularityRolePlayerController::ASingularityRolePlayerController()
|
| 9 |
+
{
|
| 10 |
+
PrimaryActorTick.bCanEverTick = true;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
void ASingularityRolePlayerController::BeginPlay()
|
| 14 |
+
{
|
| 15 |
+
Super::BeginPlay();
|
| 16 |
+
if (const ASingularityGameMode* Mode = GetWorld()->GetAuthGameMode<ASingularityGameMode>())
|
| 17 |
+
{
|
| 18 |
+
SpacetimeBridge = Mode->SpacetimeBridge;
|
| 19 |
+
}
|
| 20 |
+
if (!SpacetimeBridge)
|
| 21 |
+
{
|
| 22 |
+
// Clients do not own an authoritative GameMode, so they get a local
|
| 23 |
+
// bridge instance that their transport adapter can bind to.
|
| 24 |
+
SpacetimeBridge = NewObject<USingularitySpacetimeBridge>(this);
|
| 25 |
+
}
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
void ASingularityRolePlayerController::SetupInputComponent()
|
| 29 |
+
{
|
| 30 |
+
Super::SetupInputComponent();
|
| 31 |
+
check(InputComponent);
|
| 32 |
+
InputComponent->BindAxis(TEXT("Singularity_MoveX"), this, &ASingularityRolePlayerController::HandleMoveX);
|
| 33 |
+
InputComponent->BindAxis(TEXT("Singularity_MoveZ"), this, &ASingularityRolePlayerController::HandleMoveZ);
|
| 34 |
+
InputComponent->BindAction(TEXT("Singularity_Action"), IE_Pressed, this, &ASingularityRolePlayerController::HandleActionPressed);
|
| 35 |
+
InputComponent->BindAction(TEXT("Singularity_Action"), IE_Released, this, &ASingularityRolePlayerController::HandleActionReleased);
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
void ASingularityRolePlayerController::Tick(float DeltaSeconds)
|
| 39 |
+
{
|
| 40 |
+
Super::Tick(DeltaSeconds);
|
| 41 |
+
if (!IsLocalController()) return;
|
| 42 |
+
SendAccumulator += DeltaSeconds;
|
| 43 |
+
if (SpacetimeBridge && SendAccumulator >= (1.0f / 30.0f))
|
| 44 |
+
{
|
| 45 |
+
SendAccumulator = 0.0f;
|
| 46 |
+
++ClientTick;
|
| 47 |
+
SpacetimeBridge->SubmitRoleInput(ControlledRole, MoveX, MoveZ, bAction, ClientTick);
|
| 48 |
+
}
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
void ASingularityRolePlayerController::HandleMoveX(float Value)
|
| 52 |
+
{
|
| 53 |
+
MoveX = FMath::Clamp(Value, -1.0f, 1.0f);
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
void ASingularityRolePlayerController::HandleMoveZ(float Value)
|
| 57 |
+
{
|
| 58 |
+
MoveZ = FMath::Clamp(Value, -1.0f, 1.0f);
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
void ASingularityRolePlayerController::HandleActionPressed()
|
| 62 |
+
{
|
| 63 |
+
bAction = true;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
void ASingularityRolePlayerController::HandleActionReleased()
|
| 67 |
+
{
|
| 68 |
+
bAction = false;
|
| 69 |
+
}
|
native/unreal/Source/Singularity/SingularityRolePlayerController.h
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#pragma once
|
| 2 |
+
|
| 3 |
+
#include "CoreMinimal.h"
|
| 4 |
+
#include "GameFramework/PlayerController.h"
|
| 5 |
+
#include "SingularityRoleTypes.h"
|
| 6 |
+
#include "SingularityRolePlayerController.generated.h"
|
| 7 |
+
|
| 8 |
+
class USingularitySpacetimeBridge;
|
| 9 |
+
|
| 10 |
+
/** Sends only the local role's bounded input to the shared bridge. */
|
| 11 |
+
UCLASS()
|
| 12 |
+
class SINGULARITY_API ASingularityRolePlayerController : public APlayerController
|
| 13 |
+
{
|
| 14 |
+
GENERATED_BODY()
|
| 15 |
+
|
| 16 |
+
public:
|
| 17 |
+
ASingularityRolePlayerController();
|
| 18 |
+
|
| 19 |
+
virtual void BeginPlay() override;
|
| 20 |
+
virtual void SetupInputComponent() override;
|
| 21 |
+
virtual void Tick(float DeltaSeconds) override;
|
| 22 |
+
|
| 23 |
+
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Singularity|Role")
|
| 24 |
+
ESingularityBodyRole ControlledRole = ESingularityBodyRole::EyesHead;
|
| 25 |
+
|
| 26 |
+
UPROPERTY(BlueprintReadOnly, Category = "Singularity|Role")
|
| 27 |
+
TObjectPtr<USingularitySpacetimeBridge> SpacetimeBridge;
|
| 28 |
+
|
| 29 |
+
private:
|
| 30 |
+
float MoveX = 0.0f;
|
| 31 |
+
float MoveZ = 0.0f;
|
| 32 |
+
bool bAction = false;
|
| 33 |
+
int64 ClientTick = 0;
|
| 34 |
+
float SendAccumulator = 0.0f;
|
| 35 |
+
|
| 36 |
+
void HandleMoveX(float Value);
|
| 37 |
+
void HandleMoveZ(float Value);
|
| 38 |
+
void HandleActionPressed();
|
| 39 |
+
void HandleActionReleased();
|
| 40 |
+
};
|
native/unreal/Source/Singularity/SingularityRoleTypes.h
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#pragma once
|
| 2 |
+
|
| 3 |
+
#include "CoreMinimal.h"
|
| 4 |
+
#include "SingularityRoleTypes.generated.h"
|
| 5 |
+
|
| 6 |
+
UENUM(BlueprintType)
|
| 7 |
+
enum class ESingularityBodyRole : uint8
|
| 8 |
+
{
|
| 9 |
+
EyesHead,
|
| 10 |
+
LeftArm,
|
| 11 |
+
RightArm,
|
| 12 |
+
TorsoBack,
|
| 13 |
+
LeftLeg,
|
| 14 |
+
RightLeg
|
| 15 |
+
};
|
| 16 |
+
|
| 17 |
+
USTRUCT(BlueprintType)
|
| 18 |
+
struct SINGULARITY_API FSingularityRoleInput
|
| 19 |
+
{
|
| 20 |
+
GENERATED_BODY()
|
| 21 |
+
|
| 22 |
+
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
| 23 |
+
ESingularityBodyRole Role = ESingularityBodyRole::EyesHead;
|
| 24 |
+
|
| 25 |
+
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
| 26 |
+
float X = 0.0f;
|
| 27 |
+
|
| 28 |
+
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
| 29 |
+
float Z = 0.0f;
|
| 30 |
+
|
| 31 |
+
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
| 32 |
+
bool bAction = false;
|
| 33 |
+
|
| 34 |
+
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
| 35 |
+
int64 ClientTick = 0;
|
| 36 |
+
};
|
| 37 |
+
|
| 38 |
+
USTRUCT(BlueprintType)
|
| 39 |
+
struct SINGULARITY_API FSingularityTeamSnapshot
|
| 40 |
+
{
|
| 41 |
+
GENERATED_BODY()
|
| 42 |
+
|
| 43 |
+
// The server sends the six physics nodes in shared/physics.ts order:
|
| 44 |
+
// torso, head, left arm, right arm, left leg, right leg.
|
| 45 |
+
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
| 46 |
+
TArray<FVector> Nodes;
|
| 47 |
+
|
| 48 |
+
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
| 49 |
+
int32 CheckpointStage = 0;
|
| 50 |
+
|
| 51 |
+
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
| 52 |
+
int32 Falls = 0;
|
| 53 |
+
|
| 54 |
+
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
| 55 |
+
int64 ServerTick = 0;
|
| 56 |
+
|
| 57 |
+
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
| 58 |
+
int64 FinishTimeMs = 0;
|
| 59 |
+
|
| 60 |
+
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
| 61 |
+
bool bDelivered = false;
|
| 62 |
+
|
| 63 |
+
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
| 64 |
+
bool bFinished = false;
|
| 65 |
+
};
|
native/unreal/Source/Singularity/SingularitySharedBody.cpp
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#include "SingularitySharedBody.h"
|
| 2 |
+
|
| 3 |
+
#include "Components/SceneComponent.h"
|
| 4 |
+
#include "Components/StaticMeshComponent.h"
|
| 5 |
+
#include "Engine/World.h"
|
| 6 |
+
#include "Engine/StaticMesh.h"
|
| 7 |
+
#include "Materials/MaterialInterface.h"
|
| 8 |
+
#include "PhysicsEngine/PhysicsConstraintComponent.h"
|
| 9 |
+
|
| 10 |
+
namespace
|
| 11 |
+
{
|
| 12 |
+
const FName RoleNames[] = {
|
| 13 |
+
TEXT("EyesHead"), TEXT("LeftArm"), TEXT("RightArm"),
|
| 14 |
+
TEXT("TorsoBack"), TEXT("LeftLeg"), TEXT("RightLeg")};
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
ASingularitySharedBody::ASingularitySharedBody()
|
| 18 |
+
{
|
| 19 |
+
PrimaryActorTick.bCanEverTick = true;
|
| 20 |
+
|
| 21 |
+
BodyRoot = CreateDefaultSubobject<USceneComponent>(TEXT("BodyRoot"));
|
| 22 |
+
RootComponent = BodyRoot;
|
| 23 |
+
|
| 24 |
+
for (int32 Index = 0; Index < UE_ARRAY_COUNT(RoleNames); ++Index)
|
| 25 |
+
{
|
| 26 |
+
UStaticMeshComponent* Part = CreateDefaultSubobject<UStaticMeshComponent>(RoleNames[Index]);
|
| 27 |
+
Part->SetupAttachment(BodyRoot);
|
| 28 |
+
// Ranked presentation follows server snapshots, never local Chaos forces.
|
| 29 |
+
Part->SetSimulatePhysics(false);
|
| 30 |
+
const FVector Rest[] = {FVector(0,0,300), FVector(0,-100,200),
|
| 31 |
+
FVector(0,100,200), FVector(0,0,200), FVector(0,-45,35), FVector(0,45,35)};
|
| 32 |
+
Part->SetRelativeLocation(Rest[Index]);
|
| 33 |
+
Part->SetEnableGravity(true);
|
| 34 |
+
Part->SetCollisionProfileName(TEXT("PhysicsActor"));
|
| 35 |
+
RoleBodies.Add(Part);
|
| 36 |
+
CurrentInputs.Add(FSingularityRoleInput());
|
| 37 |
+
InputAge.Add(1.0f);
|
| 38 |
+
PreviousAction.Add(false);
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
const FName ConstraintNames[] = {
|
| 42 |
+
TEXT("ConstraintTorsoHead"), TEXT("ConstraintTorsoLeftArm"),
|
| 43 |
+
TEXT("ConstraintTorsoRightArm"), TEXT("ConstraintTorsoLeftLeg"),
|
| 44 |
+
TEXT("ConstraintTorsoRightLeg"), TEXT("ConstraintLegs"),
|
| 45 |
+
TEXT("ConstraintHeadLeftArm"), TEXT("ConstraintHeadRightArm")};
|
| 46 |
+
for (const FName& Name : ConstraintNames)
|
| 47 |
+
{
|
| 48 |
+
UPhysicsConstraintComponent* Constraint = CreateDefaultSubobject<UPhysicsConstraintComponent>(Name);
|
| 49 |
+
Constraint->SetupAttachment(BodyRoot);
|
| 50 |
+
Constraint->SetDisableCollision(true);
|
| 51 |
+
Constraints.Add(Constraint);
|
| 52 |
+
}
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
void ASingularitySharedBody::BeginPlay()
|
| 56 |
+
{
|
| 57 |
+
Super::BeginPlay();
|
| 58 |
+
LoadImportedArt();
|
| 59 |
+
ConfigureConstraints();
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
void ASingularitySharedBody::LoadImportedArt()
|
| 63 |
+
{
|
| 64 |
+
for (int32 Index = 0; Index < RoleBodies.Num(); ++Index)
|
| 65 |
+
{
|
| 66 |
+
if (RoleMeshes.IsValidIndex(Index))
|
| 67 |
+
{
|
| 68 |
+
if (UStaticMesh* Mesh = RoleMeshes[Index].LoadSynchronous())
|
| 69 |
+
{
|
| 70 |
+
RoleBodies[Index]->SetStaticMesh(Mesh);
|
| 71 |
+
}
|
| 72 |
+
}
|
| 73 |
+
if (UMaterialInterface* Material = TeamMaterial.LoadSynchronous())
|
| 74 |
+
{
|
| 75 |
+
for (int32 Slot = 0; Slot < RoleBodies[Index]->GetNumMaterials(); ++Slot)
|
| 76 |
+
{
|
| 77 |
+
const UMaterialInterface* Existing = RoleBodies[Index]->GetMaterial(Slot);
|
| 78 |
+
if (Existing && (Existing->GetName().Contains(TEXT("TeamCoral")) ||
|
| 79 |
+
Existing->GetName().Contains(TEXT("TeamMint"))))
|
| 80 |
+
RoleBodies[Index]->SetMaterial(Slot, Material);
|
| 81 |
+
}
|
| 82 |
+
}
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
void ASingularitySharedBody::ConfigureConstraints()
|
| 87 |
+
{
|
| 88 |
+
if (RoleBodies.Num() != 6 || Constraints.Num() != 8)
|
| 89 |
+
{
|
| 90 |
+
return;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
const int32 Pairs[][2] = {{3, 0}, {3, 1}, {3, 2}, {3, 4},
|
| 94 |
+
{3, 5}, {4, 5}, {0, 1}, {0, 2}};
|
| 95 |
+
for (int32 Index = 0; Index < Constraints.Num(); ++Index)
|
| 96 |
+
{
|
| 97 |
+
const FVector A = RoleBodies[Pairs[Index][0]]->GetComponentLocation();
|
| 98 |
+
const FVector B = RoleBodies[Pairs[Index][1]]->GetComponentLocation();
|
| 99 |
+
Constraints[Index]->SetWorldLocation((A + B) * 0.5);
|
| 100 |
+
Constraints[Index]->SetLinearXLimit(LCM_Locked, 0);
|
| 101 |
+
Constraints[Index]->SetLinearYLimit(LCM_Locked, 0);
|
| 102 |
+
Constraints[Index]->SetLinearZLimit(LCM_Locked, 0);
|
| 103 |
+
Constraints[Index]->SetAngularSwing1Limit(ACM_Limited, 35);
|
| 104 |
+
Constraints[Index]->SetAngularSwing2Limit(ACM_Limited, 35);
|
| 105 |
+
Constraints[Index]->SetAngularTwistLimit(ACM_Limited, 25);
|
| 106 |
+
Constraints[Index]->SetConstrainedComponents(
|
| 107 |
+
RoleBodies[Pairs[Index][0]], NAME_None,
|
| 108 |
+
RoleBodies[Pairs[Index][1]], NAME_None);
|
| 109 |
+
}
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
void ASingularitySharedBody::Tick(float DeltaSeconds)
|
| 113 |
+
{
|
| 114 |
+
Super::Tick(DeltaSeconds);
|
| 115 |
+
|
| 116 |
+
for (int32 Index = 0; Index < RoleBodies.Num(); ++Index)
|
| 117 |
+
{
|
| 118 |
+
const FSingularityRoleInput& Input = CurrentInputs[Index];
|
| 119 |
+
const ESingularityBodyRole Role = static_cast<ESingularityBodyRole>(Index);
|
| 120 |
+
InputAge[Index] += DeltaSeconds;
|
| 121 |
+
if (!RoleBodies[Index]->IsSimulatingPhysics()) continue;
|
| 122 |
+
if (InputAge[Index] > 0.5f) { PreviousAction[Index] = false; continue; }
|
| 123 |
+
const FVector Force(Input.Z * RoleForce, Input.X * RoleForce, 0.0f);
|
| 124 |
+
RoleBodies[Index]->AddForce(Force);
|
| 125 |
+
RoleBodies[Index]->AddTorqueInRadians(FVector(0.0f, Input.X * RoleForce * 0.08f, 0.0f));
|
| 126 |
+
|
| 127 |
+
FHitResult Ground;
|
| 128 |
+
const FVector Foot = RoleBodies[Index]->GetComponentLocation();
|
| 129 |
+
FCollisionQueryParams Query;
|
| 130 |
+
Query.AddIgnoredActor(this);
|
| 131 |
+
const bool bGrounded = GetWorld()->LineTraceSingleByChannel(
|
| 132 |
+
Ground, Foot, Foot - FVector(0,0,55), ECC_Visibility, Query);
|
| 133 |
+
if (Input.bAction && !PreviousAction[Index] && IsLeg(Role) && bGrounded)
|
| 134 |
+
{
|
| 135 |
+
RoleBodies[Index]->AddImpulse(FVector(0.0f, 0.0f, LegHopImpulse));
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
PreviousAction[Index] = Input.bAction;
|
| 139 |
+
}
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
void ASingularitySharedBody::ApplyRoleInput(ESingularityBodyRole Role, const FSingularityRoleInput& Input)
|
| 143 |
+
{
|
| 144 |
+
const int32 Index = RoleIndex(Role);
|
| 145 |
+
if (CurrentInputs.IsValidIndex(Index))
|
| 146 |
+
{
|
| 147 |
+
if (!FMath::IsFinite(Input.X) || !FMath::IsFinite(Input.Z)) return;
|
| 148 |
+
InputAge[Index] = 0.0f;
|
| 149 |
+
CurrentInputs[Index] = Input;
|
| 150 |
+
CurrentInputs[Index].X = FMath::Clamp(CurrentInputs[Index].X, -1.0f, 1.0f);
|
| 151 |
+
CurrentInputs[Index].Z = FMath::Clamp(CurrentInputs[Index].Z, -1.0f, 1.0f);
|
| 152 |
+
}
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
void ASingularitySharedBody::ApplyRoleInputPacket(const FSingularityRoleInput& Input)
|
| 156 |
+
{
|
| 157 |
+
ApplyRoleInput(Input.Role, Input);
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
void ASingularitySharedBody::ApplyAuthoritativeSnapshot(const FSingularityTeamSnapshot& Snapshot)
|
| 161 |
+
{
|
| 162 |
+
if (Snapshot.Nodes.Num() != 6) return;
|
| 163 |
+
for (const FVector& Node : Snapshot.Nodes) if (Node.ContainsNaN()) return;
|
| 164 |
+
// SpacetimeDB's shared solver stores torso/head before the role list used
|
| 165 |
+
// by the Unreal actor. Keep this translation at the transport boundary.
|
| 166 |
+
const int32 NodeToRole[] = {3, 0, 1, 2, 4, 5};
|
| 167 |
+
for (int32 NodeIndex = 0; NodeIndex < Snapshot.Nodes.Num() && NodeIndex < UE_ARRAY_COUNT(NodeToRole); ++NodeIndex)
|
| 168 |
+
{
|
| 169 |
+
const int32 Role = NodeToRole[NodeIndex];
|
| 170 |
+
const FVector Node = Snapshot.Nodes[NodeIndex];
|
| 171 |
+
RoleBodies[Role]->SetSimulatePhysics(false);
|
| 172 |
+
RoleBodies[Role]->SetWorldLocation(GetActorTransform().TransformPosition(
|
| 173 |
+
FVector(Node.Z, Node.X, Node.Y) * 100.0));
|
| 174 |
+
}
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
UStaticMeshComponent* ASingularitySharedBody::GetRoleComponent(ESingularityBodyRole Role) const
|
| 178 |
+
{
|
| 179 |
+
const int32 Index = RoleIndex(Role);
|
| 180 |
+
return RoleBodies.IsValidIndex(Index) ? RoleBodies[Index] : nullptr;
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
int32 ASingularitySharedBody::RoleIndex(ESingularityBodyRole Role)
|
| 184 |
+
{
|
| 185 |
+
return static_cast<int32>(Role);
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
bool ASingularitySharedBody::IsLeg(ESingularityBodyRole Role)
|
| 189 |
+
{
|
| 190 |
+
return Role == ESingularityBodyRole::LeftLeg || Role == ESingularityBodyRole::RightLeg;
|
| 191 |
+
}
|
native/unreal/Source/Singularity/SingularitySharedBody.h
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#pragma once
|
| 2 |
+
|
| 3 |
+
#include "CoreMinimal.h"
|
| 4 |
+
#include "GameFramework/Actor.h"
|
| 5 |
+
#include "SingularityRoleTypes.h"
|
| 6 |
+
#include "SingularitySharedBody.generated.h"
|
| 7 |
+
|
| 8 |
+
class UPhysicsConstraintComponent;
|
| 9 |
+
class UStaticMesh;
|
| 10 |
+
class UStaticMeshComponent;
|
| 11 |
+
class UMaterialInterface;
|
| 12 |
+
|
| 13 |
+
/** A six-piece Chaos body that can be driven by six independent role leases. */
|
| 14 |
+
UCLASS(Blueprintable)
|
| 15 |
+
class SINGULARITY_API ASingularitySharedBody : public AActor
|
| 16 |
+
{
|
| 17 |
+
GENERATED_BODY()
|
| 18 |
+
|
| 19 |
+
public:
|
| 20 |
+
ASingularitySharedBody();
|
| 21 |
+
|
| 22 |
+
virtual void BeginPlay() override;
|
| 23 |
+
virtual void Tick(float DeltaSeconds) override;
|
| 24 |
+
|
| 25 |
+
UFUNCTION(BlueprintCallable, Category = "Singularity|Body")
|
| 26 |
+
void ApplyRoleInput(ESingularityBodyRole Role, const FSingularityRoleInput& Input);
|
| 27 |
+
|
| 28 |
+
UFUNCTION(BlueprintCallable, Category = "Singularity|Body")
|
| 29 |
+
void ApplyRoleInputPacket(const FSingularityRoleInput& Input);
|
| 30 |
+
|
| 31 |
+
UFUNCTION(BlueprintCallable, Category = "Singularity|Body")
|
| 32 |
+
void ApplyAuthoritativeSnapshot(const FSingularityTeamSnapshot& Snapshot);
|
| 33 |
+
|
| 34 |
+
UFUNCTION(BlueprintPure, Category = "Singularity|Body")
|
| 35 |
+
UStaticMeshComponent* GetRoleComponent(ESingularityBodyRole Role) const;
|
| 36 |
+
|
| 37 |
+
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Singularity|Body")
|
| 38 |
+
TObjectPtr<USceneComponent> BodyRoot;
|
| 39 |
+
|
| 40 |
+
// Order is the wire order: EyesHead, LeftArm, RightArm, TorsoBack, LeftLeg, RightLeg.
|
| 41 |
+
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Singularity|Body")
|
| 42 |
+
TArray<TObjectPtr<UStaticMeshComponent>> RoleBodies;
|
| 43 |
+
|
| 44 |
+
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Singularity|Body")
|
| 45 |
+
TArray<TObjectPtr<UPhysicsConstraintComponent>> Constraints;
|
| 46 |
+
|
| 47 |
+
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Singularity|Art")
|
| 48 |
+
TArray<TSoftObjectPtr<UStaticMesh>> RoleMeshes;
|
| 49 |
+
|
| 50 |
+
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Singularity|Art")
|
| 51 |
+
TSoftObjectPtr<UMaterialInterface> TeamMaterial;
|
| 52 |
+
|
| 53 |
+
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Singularity|Physics")
|
| 54 |
+
float RoleForce = 120000.0f;
|
| 55 |
+
|
| 56 |
+
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Singularity|Physics")
|
| 57 |
+
float LegHopImpulse = 350.0f;
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
private:
|
| 61 |
+
TArray<float> InputAge;
|
| 62 |
+
TArray<bool> PreviousAction;
|
| 63 |
+
TArray<FSingularityRoleInput> CurrentInputs;
|
| 64 |
+
|
| 65 |
+
static int32 RoleIndex(ESingularityBodyRole Role);
|
| 66 |
+
static bool IsLeg(ESingularityBodyRole Role);
|
| 67 |
+
void LoadImportedArt();
|
| 68 |
+
void ConfigureConstraints();
|
| 69 |
+
};
|
native/unreal/Source/Singularity/SingularitySpacetimeBridge.cpp
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#include "SingularitySpacetimeBridge.h"
|
| 2 |
+
|
| 3 |
+
void USingularitySpacetimeBridge::SubmitRoleInput(
|
| 4 |
+
ESingularityBodyRole Role,
|
| 5 |
+
float X,
|
| 6 |
+
float Z,
|
| 7 |
+
bool bAction,
|
| 8 |
+
int64 ClientTick)
|
| 9 |
+
{
|
| 10 |
+
FSingularityRoleInput Input;
|
| 11 |
+
if (static_cast<uint8>(Role) > 5 || !FMath::IsFinite(X) || !FMath::IsFinite(Z)) return;
|
| 12 |
+
Input.Role = Role;
|
| 13 |
+
Input.X = FMath::Clamp(X, -1.0f, 1.0f);
|
| 14 |
+
Input.Z = FMath::Clamp(Z, -1.0f, 1.0f);
|
| 15 |
+
Input.bAction = bAction;
|
| 16 |
+
Input.ClientTick = ClientTick;
|
| 17 |
+
OnInputReady.Broadcast(Input);
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
void USingularitySpacetimeBridge::ApplyTeamSnapshot(const FSingularityTeamSnapshot& Snapshot)
|
| 21 |
+
{
|
| 22 |
+
if (Snapshot.Nodes.Num() != 6) return;
|
| 23 |
+
for (const FVector& Node : Snapshot.Nodes) if (Node.ContainsNaN()) return;
|
| 24 |
+
// Snapshots are server-owned. Consumers should apply only the six nodes
|
| 25 |
+
// and status fields supplied here; no client-side completion is accepted.
|
| 26 |
+
OnSnapshotReady.Broadcast(Snapshot);
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
FString USingularitySpacetimeBridge::RoleToWireName(ESingularityBodyRole Role)
|
| 30 |
+
{
|
| 31 |
+
switch (Role)
|
| 32 |
+
{
|
| 33 |
+
case ESingularityBodyRole::EyesHead: return TEXT("0");
|
| 34 |
+
case ESingularityBodyRole::LeftArm: return TEXT("1");
|
| 35 |
+
case ESingularityBodyRole::RightArm: return TEXT("2");
|
| 36 |
+
case ESingularityBodyRole::TorsoBack: return TEXT("3");
|
| 37 |
+
case ESingularityBodyRole::LeftLeg: return TEXT("4");
|
| 38 |
+
case ESingularityBodyRole::RightLeg: return TEXT("5");
|
| 39 |
+
default: return TEXT("unknown");
|
| 40 |
+
}
|
| 41 |
+
}
|
native/unreal/Source/Singularity/SingularitySpacetimeBridge.h
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#pragma once
|
| 2 |
+
|
| 3 |
+
#include "CoreMinimal.h"
|
| 4 |
+
#include "UObject/Object.h"
|
| 5 |
+
#include "SingularityRoleTypes.h"
|
| 6 |
+
#include "SingularitySpacetimeBridge.generated.h"
|
| 7 |
+
|
| 8 |
+
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FSingularityInputReady, const FSingularityRoleInput&, Input);
|
| 9 |
+
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FSingularitySnapshotReady, const FSingularityTeamSnapshot&, Snapshot);
|
| 10 |
+
|
| 11 |
+
/**
|
| 12 |
+
* Small engine-side seam for the existing SpacetimeDB contract.
|
| 13 |
+
*
|
| 14 |
+
* The production browser client submits bounded x/z/action inputs and receives
|
| 15 |
+
* authoritative team snapshots. An Unreal transport can bind to these events
|
| 16 |
+
* without ever accepting a client pose, checkpoint, or finish time.
|
| 17 |
+
*/
|
| 18 |
+
UCLASS(BlueprintType)
|
| 19 |
+
class SINGULARITY_API USingularitySpacetimeBridge : public UObject
|
| 20 |
+
{
|
| 21 |
+
GENERATED_BODY()
|
| 22 |
+
|
| 23 |
+
public:
|
| 24 |
+
UPROPERTY(BlueprintAssignable)
|
| 25 |
+
FSingularityInputReady OnInputReady;
|
| 26 |
+
|
| 27 |
+
UPROPERTY(BlueprintAssignable)
|
| 28 |
+
FSingularitySnapshotReady OnSnapshotReady;
|
| 29 |
+
|
| 30 |
+
UFUNCTION(BlueprintCallable, Category = "Singularity|Network")
|
| 31 |
+
void SubmitRoleInput(ESingularityBodyRole Role, float X, float Z, bool bAction, int64 ClientTick);
|
| 32 |
+
|
| 33 |
+
UFUNCTION(BlueprintCallable, Category = "Singularity|Network")
|
| 34 |
+
void ApplyTeamSnapshot(const FSingularityTeamSnapshot& Snapshot);
|
| 35 |
+
|
| 36 |
+
UFUNCTION(BlueprintPure, Category = "Singularity|Network")
|
| 37 |
+
static FString RoleToWireName(ESingularityBodyRole Role);
|
| 38 |
+
};
|
native/unreal/Source/SingularityEditor.Target.cs
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
using UnrealBuildTool;
|
| 2 |
+
using System.Collections.Generic;
|
| 3 |
+
|
| 4 |
+
public class SingularityEditorTarget : TargetRules
|
| 5 |
+
{
|
| 6 |
+
public SingularityEditorTarget(TargetInfo Target) : base(Target)
|
| 7 |
+
{
|
| 8 |
+
Type = TargetType.Editor;
|
| 9 |
+
DefaultBuildSettings = BuildSettingsVersion.V5;
|
| 10 |
+
IncludeOrderVersion = EngineIncludeOrderVersion.Unreal5_4;
|
| 11 |
+
ExtraModuleNames.Add("Singularity");
|
| 12 |
+
}
|
| 13 |
+
}
|