diff --git a/examples/unrealengine/.gitignore b/examples/unrealengine/.gitignore new file mode 100644 index 0000000000..7c8d42182f --- /dev/null +++ b/examples/unrealengine/.gitignore @@ -0,0 +1,92 @@ +# Visual Studio 2015 user specific files +.vs/ + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Keep Compiled Static libraries for Plugins to save trouble for others +!Plugins/**/*.lai +!Plugins/**/*.la +!Plugins/**/*.a +!Plugins/**/*.lib + +# Executables +*.exe +*.out +*.app +*.ipa + +# These project files can be generated by the engine +*.xcodeproj +*.xcworkspace +*.sln +*.suo +*.opensdf +*.sdf +*.VC.db +*.VC.opendb + +# Precompiled Assets +SourceArt/**/*.png +SourceArt/**/*.tga + +# Binary Files +Binaries/* +Plugins/*/Binaries/* + +# Builds +Build/* + +# Whitelist PakBlacklist-.txt files +!Build/*/ +Build/*/** +!Build/*/PakBlacklist*.txt + +# Don't ignore icon files in Build +!Build/**/*.ico + +# Built data for maps +*_BuiltData.uasset + +# Configuration files generated by the Editor +Saved/* + +# Compiled source files for the engine to use +Intermediate/* +Plugins/*/Intermediate/* + +# Cache files for the editor to use +DerivedDataCache/* + +# Obsidian files +.obsidian/ + +# Packaged Files +Package/ + +# Zip files +*.zip + +# Rider +.idea/ \ No newline at end of file diff --git a/examples/unrealengine/AgonesExample.uproject b/examples/unrealengine/AgonesExample.uproject new file mode 100644 index 0000000000..86c12346f8 --- /dev/null +++ b/examples/unrealengine/AgonesExample.uproject @@ -0,0 +1,16 @@ +{ + "FileVersion": 3, + "EngineAssociation": "4.26", + "Category": "", + "Description": "", + "Modules": [ + { + "Name": "AgonesExample", + "Type": "Runtime", + "LoadingPhase": "Default", + "AdditionalDependencies": [ + "Engine" + ] + } + ] +} \ No newline at end of file diff --git a/examples/unrealengine/Config/DefaultEditor.ini b/examples/unrealengine/Config/DefaultEditor.ini new file mode 100644 index 0000000000..1129739285 --- /dev/null +++ b/examples/unrealengine/Config/DefaultEditor.ini @@ -0,0 +1,8 @@ +[UnrealEd.SimpleMap] +SimpleMapName=/Game/FirstPerson/Maps/FirstPersonExampleMap + +[EditoronlyBP] +bAllowClassAndBlueprintPinMatching=true +bReplaceBlueprintWithClass= true +bDontLoadBlueprintOutsideEditor= true +bBlueprintIsNotBlueprintType= true \ No newline at end of file diff --git a/examples/unrealengine/Config/DefaultEditorPerProjectUserSettings.ini b/examples/unrealengine/Config/DefaultEditorPerProjectUserSettings.ini new file mode 100644 index 0000000000..f9a660b41b --- /dev/null +++ b/examples/unrealengine/Config/DefaultEditorPerProjectUserSettings.ini @@ -0,0 +1,2 @@ +[ContentBrowser] +ContentBrowserTab1.SelectedPaths=/Game/FirstPerson \ No newline at end of file diff --git a/examples/unrealengine/Config/DefaultEngine.ini b/examples/unrealengine/Config/DefaultEngine.ini new file mode 100644 index 0000000000..ff572e3a04 --- /dev/null +++ b/examples/unrealengine/Config/DefaultEngine.ini @@ -0,0 +1,36 @@ +[/Script/Engine.CollisionProfile] ++Profiles=(Name="Projectile",CollisionEnabled=QueryOnly,ObjectTypeName="Projectile",CustomResponses=,HelpMessage="Preset for projectiles",bCanModify=True) ++DefaultChannelResponses=(Channel=ECC_GameTraceChannel1,Name="Projectile",DefaultResponse=ECR_Block,bTraceType=False,bStaticObject=False) ++EditProfiles=(Name="Trigger",CustomResponses=((Channel=Projectile, Response=ECR_Ignore))) + +[/Script/EngineSettings.GameMapsSettings] +EditorStartupMap=/Game/FirstPersonCPP/Maps/EntryLevel.EntryLevel +LocalMapOptions= +TransitionMap= +bUseSplitscreen=True +TwoPlayerSplitscreenLayout=Horizontal +ThreePlayerSplitscreenLayout=FavorTop +GameInstanceClass=/Script/Engine.GameInstance +GameDefaultMap=/Game/FirstPersonCPP/Maps/EntryLevel.EntryLevel +ServerDefaultMap=/Game/FirstPersonCPP/Maps/EntryLevel.EntryLevel +GlobalDefaultGameMode=/Script/AgonesExample.AgonesExampleGameMode +GlobalDefaultServerGameMode=None + +[/Script/IOSRuntimeSettings.IOSRuntimeSettings] +MinimumiOSVersion=IOS_12 + + +[/Script/HardwareTargeting.HardwareTargetingSettings] +TargetedHardwareClass=Desktop +AppliedTargetedHardwareClass=Desktop +DefaultGraphicsPerformance=Maximum +AppliedDefaultGraphicsPerformance=Maximum + +[/Script/Engine.Engine] ++ActiveGameNameRedirects=(OldGameName="TP_FirstPerson",NewGameName="/Script/AgonesExample") ++ActiveGameNameRedirects=(OldGameName="/Script/TP_FirstPerson",NewGameName="/Script/AgonesExample") ++ActiveClassRedirects=(OldClassName="TP_FirstPersonProjectile",NewClassName="AgonesExampleProjectile") ++ActiveClassRedirects=(OldClassName="TP_FirstPersonHUD",NewClassName="AgonesExampleHUD") ++ActiveClassRedirects=(OldClassName="TP_FirstPersonGameMode",NewClassName="AgonesExampleGameMode") ++ActiveClassRedirects=(OldClassName="TP_FirstPersonCharacter",NewClassName="AgonesExampleCharacter") + diff --git a/examples/unrealengine/Config/DefaultGame.ini b/examples/unrealengine/Config/DefaultGame.ini new file mode 100644 index 0000000000..b250ee218d --- /dev/null +++ b/examples/unrealengine/Config/DefaultGame.ini @@ -0,0 +1,6 @@ +[ProjectSettings] +ProjectID=(A=1823396784,B=1298598689,C=1743498150,D=-2048051708) +ProjectName=First Person Template + +[/Script/EngineSettings.GeneralProjectSettings] +ProjectID=B8F9C18E44B1814CF95410BD782BCB03 diff --git a/examples/unrealengine/Config/DefaultInput.ini b/examples/unrealengine/Config/DefaultInput.ini new file mode 100644 index 0000000000..5063fc1347 --- /dev/null +++ b/examples/unrealengine/Config/DefaultInput.ini @@ -0,0 +1,185 @@ + +[/Script/Engine.InputSettings] ++AxisConfig=(AxisKeyName="Gamepad_LeftX",AxisProperties=(DeadZone=0.250000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Gamepad_LeftY",AxisProperties=(DeadZone=0.250000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Gamepad_RightX",AxisProperties=(DeadZone=0.250000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Gamepad_RightY",AxisProperties=(DeadZone=0.250000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MouseX",AxisProperties=(DeadZone=0.000000,Sensitivity=0.070000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MouseY",AxisProperties=(DeadZone=0.000000,Sensitivity=0.070000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MotionController_Left_Thumbstick_Z",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MotionController_Right_Thumbstick_Z",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MouseWheelAxis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Gamepad_LeftTriggerAxis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Gamepad_RightTriggerAxis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MotionController_Left_Thumbstick_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MotionController_Left_Thumbstick_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MotionController_Left_TriggerAxis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MotionController_Left_Grip1Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MotionController_Left_Grip2Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MotionController_Right_Thumbstick_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MotionController_Right_Thumbstick_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MotionController_Right_TriggerAxis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MotionController_Right_Grip1Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MotionController_Right_Grip2Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Gamepad_Special_Left_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Gamepad_Special_Left_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Left_Thumbstick",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Left_FaceButton1",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Left_Trigger",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Left_FaceButton2",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Left_IndexPointing",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Left_ThumbUp",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Right_Thumbstick",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Right_FaceButton1",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Right_Trigger",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Right_FaceButton2",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Right_IndexPointing",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Right_ThumbUp",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouchpad_Touchpad_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouchpad_Touchpad_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="SteamVR_Knuckles_Left_HandGrip",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="SteamVR_Knuckles_Left_IndexGrip",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="SteamVR_Knuckles_Left_MiddleGrip",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="SteamVR_Knuckles_Left_RingGrip",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="SteamVR_Knuckles_Left_PinkyGrip",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="SteamVR_Knuckles_Right_HandGrip",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="SteamVR_Knuckles_Right_IndexGrip",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="SteamVR_Knuckles_Right_MiddleGrip",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="SteamVR_Knuckles_Right_RingGrip",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="SteamVR_Knuckles_Right_PinkyGrip",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Daydream_Left_Trackpad_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Daydream_Left_Trackpad_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Daydream_Right_Trackpad_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Daydream_Right_Trackpad_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Vive_Left_Trigger_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Vive_Left_Trackpad_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Vive_Left_Trackpad_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Vive_Right_Trigger_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Vive_Right_Trackpad_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="Vive_Right_Trackpad_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MixedReality_Left_Trigger_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MixedReality_Left_Thumbstick_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MixedReality_Left_Thumbstick_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MixedReality_Left_Trackpad_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MixedReality_Left_Trackpad_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MixedReality_Right_Trigger_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MixedReality_Right_Thumbstick_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MixedReality_Right_Thumbstick_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MixedReality_Right_Trackpad_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MixedReality_Right_Trackpad_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusGo_Left_Trackpad_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusGo_Left_Trackpad_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusGo_Right_Trackpad_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusGo_Right_Trackpad_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Left_Grip_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Left_Trigger_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Left_Thumbstick_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Left_Thumbstick_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Right_Grip_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Right_Trigger_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Right_Thumbstick_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="OculusTouch_Right_Thumbstick_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Left_Grip_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Left_Grip_Force",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Left_Trigger_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Left_Thumbstick_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Left_Thumbstick_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Left_Trackpad_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Left_Trackpad_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Left_Trackpad_Force",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Left_Trackpad_Touch",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Right_Grip_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Right_Grip_Force",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Right_Trigger_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Right_Thumbstick_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Right_Thumbstick_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Right_Trackpad_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Right_Trackpad_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="ValveIndex_Right_Trackpad_Force",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MagicLeap_Left_Trigger_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MagicLeap_Left_Trackpad_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MagicLeap_Left_Trackpad_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MagicLeap_Left_Trackpad_Force",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MagicLeap_Left_Touch1_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MagicLeap_Left_Touch1_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MagicLeap_Left_Touch1_Force",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MagicLeap_Right_Trigger_Axis",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MagicLeap_Right_Trackpad_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MagicLeap_Right_Trackpad_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MagicLeap_Right_Trackpad_Force",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MagicLeap_Right_Touch1_X",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MagicLeap_Right_Touch1_Y",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) ++AxisConfig=(AxisKeyName="MagicLeap_Right_Touch1_Force",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False)) +bAltEnterTogglesFullscreen=True +bF11TogglesFullscreen=True +bUseMouseForTouch=False +bEnableMouseSmoothing=True +bEnableFOVScaling=True +bCaptureMouseOnLaunch=True +bAlwaysShowTouchInterface=False +bShowConsoleOnFourFingerTap=True +bEnableGestureRecognizer=False +bUseAutocorrect=False +DefaultViewportMouseCaptureMode=CapturePermanently_IncludingInitialMouseDown +DefaultViewportMouseLockMode=LockOnCapture +FOVScale=0.011110 +DoubleClickTime=0.200000 ++ActionMappings=(ActionName="Jump",bShift=False,bCtrl=False,bAlt=False,bCmd=False,Key=SpaceBar) ++ActionMappings=(ActionName="Jump",bShift=False,bCtrl=False,bAlt=False,bCmd=False,Key=Gamepad_FaceButton_Bottom) ++ActionMappings=(ActionName="Jump",bShift=False,bCtrl=False,bAlt=False,bCmd=False,Key=Daydream_Left_Select_Click) ++ActionMappings=(ActionName="Fire",bShift=False,bCtrl=False,bAlt=False,bCmd=False,Key=LeftMouseButton) ++ActionMappings=(ActionName="Fire",bShift=False,bCtrl=False,bAlt=False,bCmd=False,Key=Gamepad_RightTrigger) ++ActionMappings=(ActionName="Fire",bShift=False,bCtrl=False,bAlt=False,bCmd=False,Key=Daydream_Left_Trackpad_Click) ++ActionMappings=(ActionName="ResetVR",bShift=False,bCtrl=False,bAlt=False,bCmd=False,Key=R) ++ActionMappings=(ActionName="ResetVR",bShift=False,bCtrl=False,bAlt=False,bCmd=False,Key=Vive_Left_Grip_Click) ++ActionMappings=(ActionName="Fire",bShift=False,bCtrl=False,bAlt=False,bCmd=False,Key=Vive_Right_Trigger_Click) ++ActionMappings=(ActionName="Jump",bShift=False,bCtrl=False,bAlt=False,bCmd=False,Key=Vive_Left_Trigger_Click) ++ActionMappings=(ActionName="Jump",bShift=False,bCtrl=False,bAlt=False,bCmd=False,Key=MixedReality_Left_Trigger_Click) ++ActionMappings=(ActionName="Jump",bShift=False,bCtrl=False,bAlt=False,bCmd=False,Key=OculusTouch_Left_Trigger_Click) ++ActionMappings=(ActionName="Jump",bShift=False,bCtrl=False,bAlt=False,bCmd=False,Key=ValveIndex_Left_Trigger_Click) ++ActionMappings=(ActionName="Jump",bShift=False,bCtrl=False,bAlt=False,bCmd=False,Key=MagicLeap_Left_Trigger) ++ActionMappings=(ActionName="Fire",bShift=False,bCtrl=False,bAlt=False,bCmd=False,Key=MixedReality_Right_Trigger_Click) ++ActionMappings=(ActionName="Fire",bShift=False,bCtrl=False,bAlt=False,bCmd=False,Key=OculusTouch_Right_Trigger_Click) ++ActionMappings=(ActionName="Fire",bShift=False,bCtrl=False,bAlt=False,bCmd=False,Key=ValveIndex_Right_Trigger_Click) ++ActionMappings=(ActionName="Fire",bShift=False,bCtrl=False,bAlt=False,bCmd=False,Key=MagicLeap_Left_Trackpad_Touch) ++ActionMappings=(ActionName="ResetVR",bShift=False,bCtrl=False,bAlt=False,bCmd=False,Key=MixedReality_Left_Thumbstick_Click) ++ActionMappings=(ActionName="ResetVR",bShift=False,bCtrl=False,bAlt=False,bCmd=False,Key=OculusTouch_Left_Thumbstick_Click) ++ActionMappings=(ActionName="ResetVR",bShift=False,bCtrl=False,bAlt=False,bCmd=False,Key=ValveIndex_Left_Thumbstick_Click) ++ActionMappings=(ActionName="ResetVR",bShift=False,bCtrl=False,bAlt=False,bCmd=False,Key=MagicLeap_Left_Bumper) ++ActionMappings=(ActionName="Jump",bShift=False,bCtrl=False,bAlt=False,bCmd=False,Key=MagicLeap_Right_Trigger) ++ActionMappings=(ActionName="Fire",bShift=False,bCtrl=False,bAlt=False,bCmd=False,Key=MagicLeap_Right_Trackpad_Touch) ++ActionMappings=(ActionName="ResetVR",bShift=False,bCtrl=False,bAlt=False,bCmd=False,Key=MagicLeap_Right_Bumper) ++AxisMappings=(AxisName="MoveForward",Scale=1.000000,Key=W) ++AxisMappings=(AxisName="MoveForward",Scale=-1.000000,Key=S) ++AxisMappings=(AxisName="MoveForward",Scale=1.000000,Key=Up) ++AxisMappings=(AxisName="MoveForward",Scale=-1.000000,Key=Down) ++AxisMappings=(AxisName="MoveForward",Scale=1.000000,Key=Gamepad_LeftY) ++AxisMappings=(AxisName="MoveRight",Scale=-1.000000,Key=A) ++AxisMappings=(AxisName="MoveRight",Scale=1.000000,Key=D) ++AxisMappings=(AxisName="MoveRight",Scale=1.000000,Key=Gamepad_LeftX) ++AxisMappings=(AxisName="MoveRight",Scale=1.000000,Key=Daydream_Left_Trackpad_X) ++AxisMappings=(AxisName="TurnRate",Scale=1.000000,Key=Gamepad_RightX) ++AxisMappings=(AxisName="TurnRate",Scale=-1.000000,Key=Left) ++AxisMappings=(AxisName="TurnRate",Scale=1.000000,Key=Right) ++AxisMappings=(AxisName="Turn",Scale=1.000000,Key=MouseX) ++AxisMappings=(AxisName="LookUpRate",Scale=1.000000,Key=Gamepad_RightY) ++AxisMappings=(AxisName="LookUp",Scale=-1.000000,Key=MouseY) ++AxisMappings=(AxisName="MoveForward",Scale=1.000000,Key=Daydream_Left_Trackpad_Y) ++AxisMappings=(AxisName="MoveForward",Scale=1.000000,Key=Vive_Left_Trackpad_Y) ++AxisMappings=(AxisName="MoveForward",Scale=1.000000,Key=MixedReality_Left_Thumbstick_Y) ++AxisMappings=(AxisName="MoveForward",Scale=1.000000,Key=OculusGo_Left_Trackpad_Y) ++AxisMappings=(AxisName="MoveForward",Scale=1.000000,Key=OculusTouch_Left_Thumbstick_Y) ++AxisMappings=(AxisName="MoveForward",Scale=1.000000,Key=ValveIndex_Left_Thumbstick_Y) ++AxisMappings=(AxisName="MoveRight",Scale=1.000000,Key=Vive_Left_Trackpad_X) ++AxisMappings=(AxisName="MoveRight",Scale=1.000000,Key=MixedReality_Left_Thumbstick_X) ++AxisMappings=(AxisName="MoveRight",Scale=1.000000,Key=OculusGo_Left_Trackpad_X) ++AxisMappings=(AxisName="MoveRight",Scale=1.000000,Key=OculusTouch_Left_Thumbstick_Y) ++AxisMappings=(AxisName="MoveRight",Scale=1.000000,Key=ValveIndex_Left_Thumbstick_Y) ++AxisMappings=(AxisName="MoveRight",Scale=1.000000,Key=MagicLeap_Left_Trackpad_X) ++AxisMappings=(AxisName="MoveForward",Scale=1.000000,Key=MagicLeap_Left_Trackpad_Y) ++AxisMappings=(AxisName="MoveForward",Scale=1.000000,Key=MagicLeap_Right_Trackpad_Y) ++AxisMappings=(AxisName="MoveRight",Scale=1.000000,Key=MagicLeap_Right_Trackpad_X) +DefaultTouchInterface=/Engine/MobileResources/HUD/DefaultVirtualJoysticks.DefaultVirtualJoysticks ++ConsoleKeys=Tilde + + diff --git a/examples/unrealengine/Content/FirstPerson/Animations/FirstPersonFire_Montage.uasset b/examples/unrealengine/Content/FirstPerson/Animations/FirstPersonFire_Montage.uasset new file mode 100644 index 0000000000..f4e81fd973 Binary files /dev/null and b/examples/unrealengine/Content/FirstPerson/Animations/FirstPersonFire_Montage.uasset differ diff --git a/examples/unrealengine/Content/FirstPerson/Animations/FirstPerson_AnimBP.uasset b/examples/unrealengine/Content/FirstPerson/Animations/FirstPerson_AnimBP.uasset new file mode 100644 index 0000000000..ee6bda63de Binary files /dev/null and b/examples/unrealengine/Content/FirstPerson/Animations/FirstPerson_AnimBP.uasset differ diff --git a/examples/unrealengine/Content/FirstPerson/Animations/FirstPerson_Fire.uasset b/examples/unrealengine/Content/FirstPerson/Animations/FirstPerson_Fire.uasset new file mode 100644 index 0000000000..81c8d447da Binary files /dev/null and b/examples/unrealengine/Content/FirstPerson/Animations/FirstPerson_Fire.uasset differ diff --git a/examples/unrealengine/Content/FirstPerson/Animations/FirstPerson_Idle.uasset b/examples/unrealengine/Content/FirstPerson/Animations/FirstPerson_Idle.uasset new file mode 100644 index 0000000000..ea9e0f23aa Binary files /dev/null and b/examples/unrealengine/Content/FirstPerson/Animations/FirstPerson_Idle.uasset differ diff --git a/examples/unrealengine/Content/FirstPerson/Animations/FirstPerson_JumpEnd.uasset b/examples/unrealengine/Content/FirstPerson/Animations/FirstPerson_JumpEnd.uasset new file mode 100644 index 0000000000..25af5484ab Binary files /dev/null and b/examples/unrealengine/Content/FirstPerson/Animations/FirstPerson_JumpEnd.uasset differ diff --git a/examples/unrealengine/Content/FirstPerson/Animations/FirstPerson_JumpLoop.uasset b/examples/unrealengine/Content/FirstPerson/Animations/FirstPerson_JumpLoop.uasset new file mode 100644 index 0000000000..85e14950ad Binary files /dev/null and b/examples/unrealengine/Content/FirstPerson/Animations/FirstPerson_JumpLoop.uasset differ diff --git a/examples/unrealengine/Content/FirstPerson/Animations/FirstPerson_JumpStart.uasset b/examples/unrealengine/Content/FirstPerson/Animations/FirstPerson_JumpStart.uasset new file mode 100644 index 0000000000..8d6e25bcd5 Binary files /dev/null and b/examples/unrealengine/Content/FirstPerson/Animations/FirstPerson_JumpStart.uasset differ diff --git a/examples/unrealengine/Content/FirstPerson/Animations/FirstPerson_Run.uasset b/examples/unrealengine/Content/FirstPerson/Animations/FirstPerson_Run.uasset new file mode 100644 index 0000000000..27579f4b53 Binary files /dev/null and b/examples/unrealengine/Content/FirstPerson/Animations/FirstPerson_Run.uasset differ diff --git a/examples/unrealengine/Content/FirstPerson/Audio/FirstPersonTemplateWeaponFire02.uasset b/examples/unrealengine/Content/FirstPerson/Audio/FirstPersonTemplateWeaponFire02.uasset new file mode 100644 index 0000000000..3329402c9a Binary files /dev/null and b/examples/unrealengine/Content/FirstPerson/Audio/FirstPersonTemplateWeaponFire02.uasset differ diff --git a/examples/unrealengine/Content/FirstPerson/Character/Materials/M_UE4Man_Body.uasset b/examples/unrealengine/Content/FirstPerson/Character/Materials/M_UE4Man_Body.uasset new file mode 100644 index 0000000000..2c81488c5e Binary files /dev/null and b/examples/unrealengine/Content/FirstPerson/Character/Materials/M_UE4Man_Body.uasset differ diff --git a/examples/unrealengine/Content/FirstPerson/Character/Materials/MaterialLayers/ML_GlossyBlack_Latex_UE4.uasset b/examples/unrealengine/Content/FirstPerson/Character/Materials/MaterialLayers/ML_GlossyBlack_Latex_UE4.uasset new file mode 100644 index 0000000000..dc8644c1a3 Binary files /dev/null and b/examples/unrealengine/Content/FirstPerson/Character/Materials/MaterialLayers/ML_GlossyBlack_Latex_UE4.uasset differ diff --git a/examples/unrealengine/Content/FirstPerson/Character/Materials/MaterialLayers/ML_Plastic_Shiny_Beige.uasset b/examples/unrealengine/Content/FirstPerson/Character/Materials/MaterialLayers/ML_Plastic_Shiny_Beige.uasset new file mode 100644 index 0000000000..6ed00345c6 Binary files /dev/null and b/examples/unrealengine/Content/FirstPerson/Character/Materials/MaterialLayers/ML_Plastic_Shiny_Beige.uasset differ diff --git a/examples/unrealengine/Content/FirstPerson/Character/Materials/MaterialLayers/ML_Plastic_Shiny_Beige_LOGO.uasset b/examples/unrealengine/Content/FirstPerson/Character/Materials/MaterialLayers/ML_Plastic_Shiny_Beige_LOGO.uasset new file mode 100644 index 0000000000..9e0a7e1f18 Binary files /dev/null and b/examples/unrealengine/Content/FirstPerson/Character/Materials/MaterialLayers/ML_Plastic_Shiny_Beige_LOGO.uasset differ diff --git a/examples/unrealengine/Content/FirstPerson/Character/Materials/MaterialLayers/ML_SoftMetal_UE4.uasset b/examples/unrealengine/Content/FirstPerson/Character/Materials/MaterialLayers/ML_SoftMetal_UE4.uasset new file mode 100644 index 0000000000..4fe91b360b Binary files /dev/null and b/examples/unrealengine/Content/FirstPerson/Character/Materials/MaterialLayers/ML_SoftMetal_UE4.uasset differ diff --git a/examples/unrealengine/Content/FirstPerson/Character/Materials/MaterialLayers/T_ML_Aluminum01.uasset b/examples/unrealengine/Content/FirstPerson/Character/Materials/MaterialLayers/T_ML_Aluminum01.uasset new file mode 100644 index 0000000000..21c9e15ac7 Binary files /dev/null and b/examples/unrealengine/Content/FirstPerson/Character/Materials/MaterialLayers/T_ML_Aluminum01.uasset differ diff --git a/examples/unrealengine/Content/FirstPerson/Character/Materials/MaterialLayers/T_ML_Aluminum01_N.uasset b/examples/unrealengine/Content/FirstPerson/Character/Materials/MaterialLayers/T_ML_Aluminum01_N.uasset new file mode 100644 index 0000000000..5a132ac85a Binary files /dev/null and b/examples/unrealengine/Content/FirstPerson/Character/Materials/MaterialLayers/T_ML_Aluminum01_N.uasset differ diff --git a/examples/unrealengine/Content/FirstPerson/Character/Materials/MaterialLayers/T_ML_Rubber_Blue_01_D.uasset b/examples/unrealengine/Content/FirstPerson/Character/Materials/MaterialLayers/T_ML_Rubber_Blue_01_D.uasset new file mode 100644 index 0000000000..5829ff9032 Binary files /dev/null and b/examples/unrealengine/Content/FirstPerson/Character/Materials/MaterialLayers/T_ML_Rubber_Blue_01_D.uasset differ diff --git a/examples/unrealengine/Content/FirstPerson/Character/Materials/MaterialLayers/T_ML_Rubber_Blue_01_N.uasset b/examples/unrealengine/Content/FirstPerson/Character/Materials/MaterialLayers/T_ML_Rubber_Blue_01_N.uasset new file mode 100644 index 0000000000..503479ca0d Binary files /dev/null and b/examples/unrealengine/Content/FirstPerson/Character/Materials/MaterialLayers/T_ML_Rubber_Blue_01_N.uasset differ diff --git a/examples/unrealengine/Content/FirstPerson/Character/Mesh/FirstPersonAnimBlueprint_Copy.uasset b/examples/unrealengine/Content/FirstPerson/Character/Mesh/FirstPersonAnimBlueprint_Copy.uasset new file mode 100644 index 0000000000..66ed09b4b8 Binary files /dev/null and b/examples/unrealengine/Content/FirstPerson/Character/Mesh/FirstPersonAnimBlueprint_Copy.uasset differ diff --git a/examples/unrealengine/Content/FirstPerson/Character/Mesh/SK_Mannequin_Arms.uasset b/examples/unrealengine/Content/FirstPerson/Character/Mesh/SK_Mannequin_Arms.uasset new file mode 100644 index 0000000000..59c50ff162 Binary files /dev/null and b/examples/unrealengine/Content/FirstPerson/Character/Mesh/SK_Mannequin_Arms.uasset differ diff --git a/examples/unrealengine/Content/FirstPerson/Character/Mesh/SK_Mannequin_Arms_PhysicsAsset.uasset b/examples/unrealengine/Content/FirstPerson/Character/Mesh/SK_Mannequin_Arms_PhysicsAsset.uasset new file mode 100644 index 0000000000..0741ea95fb Binary files /dev/null and b/examples/unrealengine/Content/FirstPerson/Character/Mesh/SK_Mannequin_Arms_PhysicsAsset.uasset differ diff --git a/examples/unrealengine/Content/FirstPerson/Character/Mesh/SK_Mannequin_Arms_Skeleton.uasset b/examples/unrealengine/Content/FirstPerson/Character/Mesh/SK_Mannequin_Arms_Skeleton.uasset new file mode 100644 index 0000000000..4ac095404a Binary files /dev/null and b/examples/unrealengine/Content/FirstPerson/Character/Mesh/SK_Mannequin_Arms_Skeleton.uasset differ diff --git a/examples/unrealengine/Content/FirstPerson/Character/Textures/UE4_LOGO_CARD.uasset b/examples/unrealengine/Content/FirstPerson/Character/Textures/UE4_LOGO_CARD.uasset new file mode 100644 index 0000000000..500d943e3e Binary files /dev/null and b/examples/unrealengine/Content/FirstPerson/Character/Textures/UE4_LOGO_CARD.uasset differ diff --git a/examples/unrealengine/Content/FirstPerson/Character/Textures/UE4_Mannequin_MAT_MASKA.uasset b/examples/unrealengine/Content/FirstPerson/Character/Textures/UE4_Mannequin_MAT_MASKA.uasset new file mode 100644 index 0000000000..6ecba03f78 Binary files /dev/null and b/examples/unrealengine/Content/FirstPerson/Character/Textures/UE4_Mannequin_MAT_MASKA.uasset differ diff --git a/examples/unrealengine/Content/FirstPerson/Character/Textures/UE4_Mannequin__normals.uasset b/examples/unrealengine/Content/FirstPerson/Character/Textures/UE4_Mannequin__normals.uasset new file mode 100644 index 0000000000..fbfce1107f Binary files /dev/null and b/examples/unrealengine/Content/FirstPerson/Character/Textures/UE4_Mannequin__normals.uasset differ diff --git a/examples/unrealengine/Content/FirstPerson/FPWeapon/Materials/M_FPGun.uasset b/examples/unrealengine/Content/FirstPerson/FPWeapon/Materials/M_FPGun.uasset new file mode 100644 index 0000000000..25076746a2 Binary files /dev/null and b/examples/unrealengine/Content/FirstPerson/FPWeapon/Materials/M_FPGun.uasset differ diff --git a/examples/unrealengine/Content/FirstPerson/FPWeapon/Materials/MaterialLayers/ML_GlossyBlack_Latex_UE4.uasset b/examples/unrealengine/Content/FirstPerson/FPWeapon/Materials/MaterialLayers/ML_GlossyBlack_Latex_UE4.uasset new file mode 100644 index 0000000000..8abd7bdd87 Binary files /dev/null and b/examples/unrealengine/Content/FirstPerson/FPWeapon/Materials/MaterialLayers/ML_GlossyBlack_Latex_UE4.uasset differ diff --git a/examples/unrealengine/Content/FirstPerson/FPWeapon/Materials/MaterialLayers/ML_Plastic_Shiny_Beige.uasset b/examples/unrealengine/Content/FirstPerson/FPWeapon/Materials/MaterialLayers/ML_Plastic_Shiny_Beige.uasset new file mode 100644 index 0000000000..c3124eeaab Binary files /dev/null and b/examples/unrealengine/Content/FirstPerson/FPWeapon/Materials/MaterialLayers/ML_Plastic_Shiny_Beige.uasset differ diff --git a/examples/unrealengine/Content/FirstPerson/FPWeapon/Materials/MaterialLayers/ML_Plastic_Shiny_Beige_LOGO.uasset b/examples/unrealengine/Content/FirstPerson/FPWeapon/Materials/MaterialLayers/ML_Plastic_Shiny_Beige_LOGO.uasset new file mode 100644 index 0000000000..f81e6a314a Binary files /dev/null and b/examples/unrealengine/Content/FirstPerson/FPWeapon/Materials/MaterialLayers/ML_Plastic_Shiny_Beige_LOGO.uasset differ diff --git a/examples/unrealengine/Content/FirstPerson/FPWeapon/Materials/MaterialLayers/ML_Screen.uasset b/examples/unrealengine/Content/FirstPerson/FPWeapon/Materials/MaterialLayers/ML_Screen.uasset new file mode 100644 index 0000000000..a93197f3a6 Binary files /dev/null and b/examples/unrealengine/Content/FirstPerson/FPWeapon/Materials/MaterialLayers/ML_Screen.uasset differ diff --git a/examples/unrealengine/Content/FirstPerson/FPWeapon/Materials/MaterialLayers/ML_SoftMetal_UE4.uasset b/examples/unrealengine/Content/FirstPerson/FPWeapon/Materials/MaterialLayers/ML_SoftMetal_UE4.uasset new file mode 100644 index 0000000000..b4064c1d5b Binary files /dev/null and b/examples/unrealengine/Content/FirstPerson/FPWeapon/Materials/MaterialLayers/ML_SoftMetal_UE4.uasset differ diff --git a/examples/unrealengine/Content/FirstPerson/FPWeapon/Materials/MaterialLayers/T_ML_Aluminum01.uasset b/examples/unrealengine/Content/FirstPerson/FPWeapon/Materials/MaterialLayers/T_ML_Aluminum01.uasset new file mode 100644 index 0000000000..abb5c8f269 Binary files /dev/null and b/examples/unrealengine/Content/FirstPerson/FPWeapon/Materials/MaterialLayers/T_ML_Aluminum01.uasset differ diff --git a/examples/unrealengine/Content/FirstPerson/FPWeapon/Materials/MaterialLayers/T_ML_Aluminum01_N.uasset b/examples/unrealengine/Content/FirstPerson/FPWeapon/Materials/MaterialLayers/T_ML_Aluminum01_N.uasset new file mode 100644 index 0000000000..91bfa7b196 Binary files /dev/null and b/examples/unrealengine/Content/FirstPerson/FPWeapon/Materials/MaterialLayers/T_ML_Aluminum01_N.uasset differ diff --git a/examples/unrealengine/Content/FirstPerson/FPWeapon/Materials/MaterialLayers/T_ML_FineRubber.uasset b/examples/unrealengine/Content/FirstPerson/FPWeapon/Materials/MaterialLayers/T_ML_FineRubber.uasset new file mode 100644 index 0000000000..809ab90248 Binary files /dev/null and b/examples/unrealengine/Content/FirstPerson/FPWeapon/Materials/MaterialLayers/T_ML_FineRubber.uasset differ diff --git a/examples/unrealengine/Content/FirstPerson/FPWeapon/Materials/MaterialLayers/T_ML_Rubber_Blue_01_D.uasset b/examples/unrealengine/Content/FirstPerson/FPWeapon/Materials/MaterialLayers/T_ML_Rubber_Blue_01_D.uasset new file mode 100644 index 0000000000..9003b8af5e Binary files /dev/null and b/examples/unrealengine/Content/FirstPerson/FPWeapon/Materials/MaterialLayers/T_ML_Rubber_Blue_01_D.uasset differ diff --git a/examples/unrealengine/Content/FirstPerson/FPWeapon/Materials/MaterialLayers/T_ML_Rubber_Blue_01_N.uasset b/examples/unrealengine/Content/FirstPerson/FPWeapon/Materials/MaterialLayers/T_ML_Rubber_Blue_01_N.uasset new file mode 100644 index 0000000000..7b8f783a12 Binary files /dev/null and b/examples/unrealengine/Content/FirstPerson/FPWeapon/Materials/MaterialLayers/T_ML_Rubber_Blue_01_N.uasset differ diff --git a/examples/unrealengine/Content/FirstPerson/FPWeapon/Mesh/SK_FPGun.uasset b/examples/unrealengine/Content/FirstPerson/FPWeapon/Mesh/SK_FPGun.uasset new file mode 100644 index 0000000000..b1cc857af2 Binary files /dev/null and b/examples/unrealengine/Content/FirstPerson/FPWeapon/Mesh/SK_FPGun.uasset differ diff --git a/examples/unrealengine/Content/FirstPerson/FPWeapon/Mesh/SK_FPGun_PhysicsAsset.uasset b/examples/unrealengine/Content/FirstPerson/FPWeapon/Mesh/SK_FPGun_PhysicsAsset.uasset new file mode 100644 index 0000000000..70ce143479 Binary files /dev/null and b/examples/unrealengine/Content/FirstPerson/FPWeapon/Mesh/SK_FPGun_PhysicsAsset.uasset differ diff --git a/examples/unrealengine/Content/FirstPerson/FPWeapon/Mesh/SK_FPGun_Skeleton.uasset b/examples/unrealengine/Content/FirstPerson/FPWeapon/Mesh/SK_FPGun_Skeleton.uasset new file mode 100644 index 0000000000..d0304a0774 Binary files /dev/null and b/examples/unrealengine/Content/FirstPerson/FPWeapon/Mesh/SK_FPGun_Skeleton.uasset differ diff --git a/examples/unrealengine/Content/FirstPerson/FPWeapon/Textures/T_FPGun_M.uasset b/examples/unrealengine/Content/FirstPerson/FPWeapon/Textures/T_FPGun_M.uasset new file mode 100644 index 0000000000..62349ed7a8 Binary files /dev/null and b/examples/unrealengine/Content/FirstPerson/FPWeapon/Textures/T_FPGun_M.uasset differ diff --git a/examples/unrealengine/Content/FirstPerson/FPWeapon/Textures/T_FPGun_N.uasset b/examples/unrealengine/Content/FirstPerson/FPWeapon/Textures/T_FPGun_N.uasset new file mode 100644 index 0000000000..6744e80682 Binary files /dev/null and b/examples/unrealengine/Content/FirstPerson/FPWeapon/Textures/T_FPGun_N.uasset differ diff --git a/examples/unrealengine/Content/FirstPerson/Meshes/BaseMaterial.uasset b/examples/unrealengine/Content/FirstPerson/Meshes/BaseMaterial.uasset new file mode 100644 index 0000000000..28877d4ce4 Binary files /dev/null and b/examples/unrealengine/Content/FirstPerson/Meshes/BaseMaterial.uasset differ diff --git a/examples/unrealengine/Content/FirstPerson/Meshes/CubeMaterialOverride.uasset b/examples/unrealengine/Content/FirstPerson/Meshes/CubeMaterialOverride.uasset new file mode 100644 index 0000000000..c38fcc26ee Binary files /dev/null and b/examples/unrealengine/Content/FirstPerson/Meshes/CubeMaterialOverride.uasset differ diff --git a/examples/unrealengine/Content/FirstPerson/Meshes/FirstPersonProjectileMaterial.uasset b/examples/unrealengine/Content/FirstPerson/Meshes/FirstPersonProjectileMaterial.uasset new file mode 100644 index 0000000000..4f2cd317ab Binary files /dev/null and b/examples/unrealengine/Content/FirstPerson/Meshes/FirstPersonProjectileMaterial.uasset differ diff --git a/examples/unrealengine/Content/FirstPerson/Meshes/FirstPersonProjectileMesh.uasset b/examples/unrealengine/Content/FirstPerson/Meshes/FirstPersonProjectileMesh.uasset new file mode 100644 index 0000000000..a738f362d4 Binary files /dev/null and b/examples/unrealengine/Content/FirstPerson/Meshes/FirstPersonProjectileMesh.uasset differ diff --git a/examples/unrealengine/Content/FirstPerson/Textures/FirstPersonCrosshair.uasset b/examples/unrealengine/Content/FirstPerson/Textures/FirstPersonCrosshair.uasset new file mode 100644 index 0000000000..68e6d9e092 Binary files /dev/null and b/examples/unrealengine/Content/FirstPerson/Textures/FirstPersonCrosshair.uasset differ diff --git a/examples/unrealengine/Content/FirstPersonCPP/Blueprints/BP_ConnectWidget.uasset b/examples/unrealengine/Content/FirstPersonCPP/Blueprints/BP_ConnectWidget.uasset new file mode 100644 index 0000000000..773429df0c Binary files /dev/null and b/examples/unrealengine/Content/FirstPersonCPP/Blueprints/BP_ConnectWidget.uasset differ diff --git a/examples/unrealengine/Content/FirstPersonCPP/Blueprints/BP_EntryGameMode.uasset b/examples/unrealengine/Content/FirstPersonCPP/Blueprints/BP_EntryGameMode.uasset new file mode 100644 index 0000000000..d278697ddb Binary files /dev/null and b/examples/unrealengine/Content/FirstPersonCPP/Blueprints/BP_EntryGameMode.uasset differ diff --git a/examples/unrealengine/Content/FirstPersonCPP/Blueprints/BP_InfoWidget.uasset b/examples/unrealengine/Content/FirstPersonCPP/Blueprints/BP_InfoWidget.uasset new file mode 100644 index 0000000000..ff524f58a6 Binary files /dev/null and b/examples/unrealengine/Content/FirstPersonCPP/Blueprints/BP_InfoWidget.uasset differ diff --git a/examples/unrealengine/Content/FirstPersonCPP/Blueprints/FirstPersonCharacter.uasset b/examples/unrealengine/Content/FirstPersonCPP/Blueprints/FirstPersonCharacter.uasset new file mode 100644 index 0000000000..8175bca39c Binary files /dev/null and b/examples/unrealengine/Content/FirstPersonCPP/Blueprints/FirstPersonCharacter.uasset differ diff --git a/examples/unrealengine/Content/FirstPersonCPP/Blueprints/FirstPersonProjectile.uasset b/examples/unrealengine/Content/FirstPersonCPP/Blueprints/FirstPersonProjectile.uasset new file mode 100644 index 0000000000..aa75c29319 Binary files /dev/null and b/examples/unrealengine/Content/FirstPersonCPP/Blueprints/FirstPersonProjectile.uasset differ diff --git a/examples/unrealengine/Content/FirstPersonCPP/Maps/EntryLevel.umap b/examples/unrealengine/Content/FirstPersonCPP/Maps/EntryLevel.umap new file mode 100644 index 0000000000..eef10517ab Binary files /dev/null and b/examples/unrealengine/Content/FirstPersonCPP/Maps/EntryLevel.umap differ diff --git a/examples/unrealengine/Content/FirstPersonCPP/Maps/ServerLevel.umap b/examples/unrealengine/Content/FirstPersonCPP/Maps/ServerLevel.umap new file mode 100644 index 0000000000..d26c411ee5 Binary files /dev/null and b/examples/unrealengine/Content/FirstPersonCPP/Maps/ServerLevel.umap differ diff --git a/examples/unrealengine/Content/Geometry/Meshes/1M_Cube.uasset b/examples/unrealengine/Content/Geometry/Meshes/1M_Cube.uasset new file mode 100644 index 0000000000..a2aff598de Binary files /dev/null and b/examples/unrealengine/Content/Geometry/Meshes/1M_Cube.uasset differ diff --git a/examples/unrealengine/Content/Geometry/Meshes/1M_Cube_Chamfer.uasset b/examples/unrealengine/Content/Geometry/Meshes/1M_Cube_Chamfer.uasset new file mode 100644 index 0000000000..cb1abf9354 Binary files /dev/null and b/examples/unrealengine/Content/Geometry/Meshes/1M_Cube_Chamfer.uasset differ diff --git a/examples/unrealengine/Content/Geometry/Meshes/CubeMaterial.uasset b/examples/unrealengine/Content/Geometry/Meshes/CubeMaterial.uasset new file mode 100644 index 0000000000..6960a6967b Binary files /dev/null and b/examples/unrealengine/Content/Geometry/Meshes/CubeMaterial.uasset differ diff --git a/examples/unrealengine/Content/Geometry/Meshes/TemplateFloor.uasset b/examples/unrealengine/Content/Geometry/Meshes/TemplateFloor.uasset new file mode 100644 index 0000000000..be4c8e0e61 Binary files /dev/null and b/examples/unrealengine/Content/Geometry/Meshes/TemplateFloor.uasset differ diff --git a/examples/unrealengine/Plugins/Agones/.gitignore b/examples/unrealengine/Plugins/Agones/.gitignore new file mode 100644 index 0000000000..c4ed221d06 --- /dev/null +++ b/examples/unrealengine/Plugins/Agones/.gitignore @@ -0,0 +1,2 @@ +Binaries/ +Intermediate/ \ No newline at end of file diff --git a/examples/unrealengine/Plugins/Agones/Agones.uplugin b/examples/unrealengine/Plugins/Agones/Agones.uplugin new file mode 100644 index 0000000000..4244e2d11a --- /dev/null +++ b/examples/unrealengine/Plugins/Agones/Agones.uplugin @@ -0,0 +1,24 @@ +{ + "CanContainContent": false, + "Category": "Agones", + "CreatedBy": "", + "CreatedByURL": "", + "Description": "Agones SDK for Unreal, wrapping the Agones REST API.", + "DocsURL": "https://agones.dev/site/docs/guides/client-sdks/unreal/", + "FileVersion": 3, + "FriendlyName": "Agones", + "Installed": false, + "IsBetaVersion": false, + "IsExperimentalVersion": false, + "MarketplaceURL": "", + "Modules": [ + { + "LoadingPhase": "PreLoadingScreen", + "Name": "Agones", + "Type": "Runtime" + } + ], + "SupportURL": "https://github.com/googleforgames/agones", + "Version": 2, + "VersionName": "2.0.0" +} diff --git a/examples/unrealengine/Plugins/Agones/Resources/Icon128.png b/examples/unrealengine/Plugins/Agones/Resources/Icon128.png new file mode 100644 index 0000000000..8e8bdc7e5d Binary files /dev/null and b/examples/unrealengine/Plugins/Agones/Resources/Icon128.png differ diff --git a/examples/unrealengine/Plugins/Agones/Source/Agones/Agones.Build.cs b/examples/unrealengine/Plugins/Agones/Source/Agones/Agones.Build.cs new file mode 100644 index 0000000000..1893efdfe9 --- /dev/null +++ b/examples/unrealengine/Plugins/Agones/Source/Agones/Agones.Build.cs @@ -0,0 +1,42 @@ +// Copyright 2020 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using UnrealBuildTool; + +public class Agones : ModuleRules +{ + public Agones(ReadOnlyTargetRules target) : base(target) + { + PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; + PublicIncludePaths.AddRange(new string[] {}); + PrivateIncludePaths.AddRange(new string[] {}); + PublicDependencyModuleNames.AddRange(new[] + { + "Core", + "Http", + "Json", + "JsonUtilities", + "WebSockets" + }); + PrivateDependencyModuleNames.AddRange( + new[] + { + "CoreUObject", + "Engine", + "Slate", + "SlateCore" + }); + DynamicallyLoadedModuleNames.AddRange(new string[]{ }); + } +} diff --git a/examples/unrealengine/Plugins/Agones/Source/Agones/Classes/Classes.h b/examples/unrealengine/Plugins/Agones/Source/Agones/Classes/Classes.h new file mode 100644 index 0000000000..ca8c613bc6 --- /dev/null +++ b/examples/unrealengine/Plugins/Agones/Source/Agones/Classes/Classes.h @@ -0,0 +1,375 @@ +// Copyright 2020 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include "CoreMinimal.h" +#include "Dom/JsonObject.h" + +#include "Classes.generated.h" + +USTRUCT(BlueprintType) +struct FObjectMeta +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadOnly) + FString Name; + + UPROPERTY(BlueprintReadOnly) + FString Namespace; + + UPROPERTY(BlueprintReadOnly) + FString Uid; + + UPROPERTY(BlueprintReadOnly) + FString ResourceVersion; + + UPROPERTY(BlueprintReadOnly) + int64 Generation = 0; + + UPROPERTY(BlueprintReadOnly) + int64 CreationTimestamp = 0; + + UPROPERTY(BlueprintReadOnly) + int64 DeletionTimestamp = 0; + + UPROPERTY(BlueprintReadOnly) + TMap Annotations; + + UPROPERTY(BlueprintReadOnly) + TMap Labels; + + FObjectMeta() + { + } + + explicit FObjectMeta(TSharedPtr JsonObject) + { + JsonObject->TryGetStringField(TEXT("name"), Name); + JsonObject->TryGetStringField(TEXT("namespace"), Namespace); + JsonObject->TryGetStringField(TEXT("uid"), Uid); + JsonObject->TryGetStringField(TEXT("resource_version"), ResourceVersion); + JsonObject->TryGetNumberField(TEXT("generation"), Generation); + JsonObject->TryGetNumberField(TEXT("creation_timestamp"), CreationTimestamp); + JsonObject->TryGetNumberField(TEXT("deletion_timestamp"), DeletionTimestamp); + const TSharedPtr* AnnotationsJsonObject; + if (JsonObject->TryGetObjectField(TEXT("annotations"), AnnotationsJsonObject)) + { + for (const auto& Entry : (*AnnotationsJsonObject)->Values) + { + if (Entry.Value.IsValid() && !Entry.Value->IsNull()) + { + FJsonValueString Key = Entry.Key; + TSharedPtr Value = Entry.Value; + FString AnnotationKey = Key.AsString(); + FString AnnotationValue = Value->AsString(); + Annotations.Add(AnnotationKey, AnnotationValue); + } + } + } + const TSharedPtr* LabelsObject; + if (JsonObject->TryGetObjectField(TEXT("labels"), LabelsObject)) + { + for (const auto& Entry : (*LabelsObject)->Values) + { + if (Entry.Value.IsValid() && !Entry.Value->IsNull()) + { + FJsonValueString Key = Entry.Key; + TSharedPtr Value = Entry.Value; + FString LabelKey = Key.AsString(); + FString LabelValue = Value->AsString(); + Labels.Add(LabelKey, LabelValue); + } + } + } + } +}; + +USTRUCT(BlueprintType) +struct FHealth +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadOnly) + bool bDisabled = false; + + UPROPERTY(BlueprintReadOnly) + int32 PeriodSeconds = 0; + + UPROPERTY(BlueprintReadOnly) + int32 FailureThreshold = 0; + + UPROPERTY(BlueprintReadOnly) + int32 InitialDelaySeconds = 0; + + FHealth() + { + } + + explicit FHealth(const TSharedPtr JsonObject) + { + JsonObject->TryGetBoolField(TEXT("disabled"), bDisabled); + JsonObject->TryGetNumberField(TEXT("period_seconds"), PeriodSeconds); + JsonObject->TryGetNumberField(TEXT("failure_threshold"), FailureThreshold); + JsonObject->TryGetNumberField(TEXT("initial_delay_seconds"), InitialDelaySeconds); + } +}; + +USTRUCT(BlueprintType) +struct FSpec +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadOnly) + FHealth Health; + + FSpec() + { + } + + explicit FSpec(const TSharedPtr JsonObject) + { + const TSharedPtr* HealthJsonObject; + if (JsonObject->TryGetObjectField(TEXT("health"), HealthJsonObject)) + { + Health = FHealth(*HealthJsonObject); + } + } +}; + +USTRUCT(BlueprintType) +struct FPort +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadOnly) + FString Name; + + UPROPERTY(BlueprintReadOnly) + int32 Port = 0; + + FPort() + { + } + + explicit FPort(const TSharedPtr JsonObject) + { + JsonObject->TryGetStringField(TEXT("name"), Name); + JsonObject->TryGetNumberField(TEXT("port"), Port); + } +}; + +USTRUCT(BlueprintType) +struct FStatus +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadOnly) + FString State; + + UPROPERTY(BlueprintReadOnly) + FString Address; + + UPROPERTY(BlueprintReadOnly) + TArray Ports; + + FStatus() + { + } + + explicit FStatus(const TSharedPtr JsonObject) + { + JsonObject->TryGetStringField(TEXT("state"), State); + JsonObject->TryGetStringField(TEXT("address"), Address); + const TArray>* PortsArray; + if (JsonObject->TryGetArrayField(TEXT("ports"), PortsArray)) + { + const int32 ArrLen = PortsArray->Num(); + for (int32 i = 0; i < ArrLen; ++i) + { + const TSharedPtr& PortItem = (*PortsArray)[i]; + if (PortItem.IsValid() && !PortItem->IsNull()) + { + FPort Port = FPort(PortItem->AsObject()); + Ports.Add(Port); + } + } + } + } +}; + +USTRUCT(BlueprintType) +struct FGameServerResponse +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadOnly) + FStatus Status; + + UPROPERTY(BlueprintReadOnly) + FObjectMeta ObjectMeta; + + UPROPERTY(BlueprintReadOnly) + FSpec Spec; + + FGameServerResponse() + { + } + + explicit FGameServerResponse(const TSharedPtr JsonObject) + { + const TSharedPtr* ObjectMetaJsonObject; + if (JsonObject->TryGetObjectField(TEXT("object_meta"), ObjectMetaJsonObject)) + { + ObjectMeta = FObjectMeta(*ObjectMetaJsonObject); + } + const TSharedPtr* SpecJsonObject; + if (JsonObject->TryGetObjectField(TEXT("spec"), SpecJsonObject)) + { + Spec = FSpec(*SpecJsonObject); + } + const TSharedPtr* StatusJsonObject; + if (JsonObject->TryGetObjectField(TEXT("status"), StatusJsonObject)) + { + Status = FStatus(*StatusJsonObject); + } + } +}; + +USTRUCT(BlueprintType) +struct FKeyValuePair +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadOnly) + FString Key; + + UPROPERTY(BlueprintReadOnly) + FString Value; +}; + +USTRUCT(BlueprintType) +struct FDuration +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadOnly) + int64 Seconds; +}; + +USTRUCT(BlueprintType) +struct FAgonesPlayer +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadOnly) + FString PlayerID; +}; + +USTRUCT(BlueprintType) +struct FPlayerCapacity +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadOnly) + int64 Count; +}; + +USTRUCT(BlueprintType) +struct FEmptyResponse +{ + GENERATED_BODY() +}; + +USTRUCT(BlueprintType) +struct FAgonesError +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadOnly) + FString ErrorMessage; +}; + +USTRUCT(BlueprintType) +struct FConnectedResponse +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadOnly) + bool bConnected = false; + + FConnectedResponse() + { + } + + explicit FConnectedResponse(const TSharedPtr JsonObject) + { + JsonObject->TryGetBoolField(TEXT("bool"), bConnected); + } +}; + +USTRUCT(BlueprintType) +struct FDisconnectResponse +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadOnly) + bool bDisconnected = false; + + FDisconnectResponse() + { + } + + explicit FDisconnectResponse(const TSharedPtr JsonObject) + { + JsonObject->TryGetBoolField(TEXT("bool"), bDisconnected); + } +}; + +USTRUCT(BlueprintType) +struct FCountResponse +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadOnly) + int64 Count = 0; + + FCountResponse() + { + } + + explicit FCountResponse(const TSharedPtr JsonObject) + { + JsonObject->TryGetNumberField(TEXT("count"), Count); + } +}; + +USTRUCT(BlueprintType) +struct FConnectedPlayersResponse +{ + GENERATED_BODY() + + FConnectedPlayersResponse() + { + } + + UPROPERTY(BlueprintReadOnly) + TArray ConnectedPlayers; + + explicit FConnectedPlayersResponse(const TSharedPtr JsonObject) + { + JsonObject->TryGetStringArrayField(TEXT("list"), ConnectedPlayers); + } +}; diff --git a/examples/unrealengine/Plugins/Agones/Source/Agones/Private/Agones.cpp b/examples/unrealengine/Plugins/Agones/Source/Agones/Private/Agones.cpp new file mode 100644 index 0000000000..eb1ebc23c1 --- /dev/null +++ b/examples/unrealengine/Plugins/Agones/Source/Agones/Private/Agones.cpp @@ -0,0 +1,32 @@ +// Copyright 2020 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "Agones.h" + +#define LOCTEXT_NAMESPACE "FAgonesModule" + +void FAgonesModule::StartupModule() +{ + // This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module +} + +void FAgonesModule::ShutdownModule() +{ + // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, + // we call this function before unloading the module. +} + +#undef LOCTEXT_NAMESPACE + +IMPLEMENT_MODULE(FAgonesModule, Agones) diff --git a/examples/unrealengine/Plugins/Agones/Source/Agones/Private/AgonesComponent.cpp b/examples/unrealengine/Plugins/Agones/Source/Agones/Private/AgonesComponent.cpp new file mode 100644 index 0000000000..da31299348 --- /dev/null +++ b/examples/unrealengine/Plugins/Agones/Source/Agones/Private/AgonesComponent.cpp @@ -0,0 +1,660 @@ +// Copyright 2020 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "AgonesComponent.h" + +#include "Engine/World.h" +#include "HttpModule.h" +#include "Interfaces/IHttpResponse.h" +#include "JsonUtilities/Public/JsonObjectConverter.h" +#include "TimerManager.h" +#include "WebSockets/Public/IWebSocket.h" +#include "WebSockets/Public/WebSocketsModule.h" + +UAgonesComponent::UAgonesComponent() +{ + PrimaryComponentTick.bCanEverTick = false; +} + +void UAgonesComponent::BeginPlay() +{ + Super::BeginPlay(); + HealthPing(HealthRateSeconds); + + if (bDisableAutoConnect) + { + return; + } + Connect(); +} + +void UAgonesComponent::EndPlay(const EEndPlayReason::Type EndPlayReason) +{ + Super::EndPlay(EndPlayReason); + + const UWorld* World = GetWorld(); + if (World != nullptr) + { + World->GetTimerManager().ClearTimer(ConnectDelTimerHandle); + World->GetTimerManager().ClearTimer(HealthTimerHandler); + World->GetTimerManager().ClearTimer(EnsureWebSocketTimerHandler); + } + + if (WatchWebSocket != nullptr && WatchWebSocket->IsConnected()) + { + WatchWebSocket->Close(); + } +} + +FHttpRequestRef UAgonesComponent::BuildAgonesRequest(const FString Path, const FHttpVerb Verb, const FString Content) +{ + FHttpModule* Http = &FHttpModule::Get(); + FHttpRequestRef Request = Http->CreateRequest(); + Request->SetURL(FString::Format(TEXT("http://localhost:{0}/{1}"), {*HttpPort, *Path})); + Request->SetVerb(Verb.ToString()); + Request->SetHeader(TEXT("Content-Type"), TEXT("application/json")); + Request->SetHeader(TEXT("User-Agent"), TEXT("X-UnrealEngine-Agent")); + Request->SetHeader(TEXT("Accepts"), TEXT("application/json")); + Request->SetContentAsString(Content); + return Request; +} + +void UAgonesComponent::HealthPing(const float RateSeconds) +{ + if (RateSeconds <= 0.0f) + { + return; + } + + FTimerDelegate TimerDel; + TimerDel.BindUObject(this, &UAgonesComponent::Health, FHealthDelegate(), FAgonesErrorDelegate()); + GetWorld()->GetTimerManager().ClearTimer(HealthTimerHandler); + GetWorld()->GetTimerManager().SetTimer(HealthTimerHandler, TimerDel, RateSeconds, true); +} + +void UAgonesComponent::Connect() +{ + FGameServerDelegate SuccessDel; + SuccessDel.BindUFunction(this, FName("ConnectSuccess")); + FTimerDelegate ConnectDel; + ConnectDel.BindUObject(this, &UAgonesComponent::GameServer, SuccessDel, FAgonesErrorDelegate()); + GetWorld()->GetTimerManager().ClearTimer(ConnectDelTimerHandle); + GetWorld()->GetTimerManager().SetTimer(ConnectDelTimerHandle, ConnectDel, 5.f, true); +} + +void UAgonesComponent::ConnectSuccess(const FGameServerResponse GameServerResponse) +{ + GetWorld()->GetTimerManager().ClearTimer(ConnectDelTimerHandle); + Ready({}, {}); + ConnectedDelegate.Broadcast(GameServerResponse); +} + +void UAgonesComponent::Ready(const FReadyDelegate SuccessDelegate, const FAgonesErrorDelegate ErrorDelegate) +{ + FHttpRequestRef Request = BuildAgonesRequest("ready"); + Request->OnProcessRequestComplete().BindWeakLambda(this, + [SuccessDelegate, ErrorDelegate](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, const bool bSucceeded) { + if (!bSucceeded) + { + ErrorDelegate.ExecuteIfBound({}); + return; + } + if (!EHttpResponseCodes::IsOk(HttpResponse->GetResponseCode())) + { + ErrorDelegate.ExecuteIfBound( + {FString::Format(TEXT("Error Code - {0}"), {FString::FromInt(HttpResponse->GetResponseCode())})}); + return; + } + + SuccessDelegate.ExecuteIfBound({}); + }); + Request->ProcessRequest(); +} + +void UAgonesComponent::GameServer(const FGameServerDelegate SuccessDelegate, const FAgonesErrorDelegate ErrorDelegate) +{ + FHttpRequestRef Request = BuildAgonesRequest("gameserver", FHttpVerb::Get, ""); + Request->OnProcessRequestComplete().BindWeakLambda(this, + [SuccessDelegate, ErrorDelegate](FHttpRequestPtr HttpRequest, const FHttpResponsePtr HttpResponse, const bool bSucceeded) { + if (!bSucceeded) + { + ErrorDelegate.ExecuteIfBound({"Unsuccessful Call"}); + return; + } + + if (!EHttpResponseCodes::IsOk(HttpResponse->GetResponseCode())) + { + ErrorDelegate.ExecuteIfBound( + {FString::Format(TEXT("Error Code - {0}"), {FString::FromInt(HttpResponse->GetResponseCode())})}); + return; + } + + const FString Json = HttpResponse->GetContentAsString(); + TSharedPtr JsonObject; + const TSharedRef> JsonReader = TJsonReaderFactory<>::Create(Json); + if (!FJsonSerializer::Deserialize(JsonReader, JsonObject) || !JsonObject.IsValid()) + { + ErrorDelegate.ExecuteIfBound({FString::Format(TEXT("Failed to parse JSON - {0}"), {*Json})}); + ErrorDelegate.ExecuteIfBound({}); + return; + } + SuccessDelegate.ExecuteIfBound(FGameServerResponse(JsonObject)); + }); + Request->ProcessRequest(); +} + +void UAgonesComponent::EnsureWebSocketConnection() +{ + if (WatchWebSocket == nullptr) + { + if (!FModuleManager::LoadModulePtr(TEXT("WebSockets"))) + { + return; + } + + TMap Headers; + + // Make up a WebSocket-Key value. It can be anything! + Headers.Add(TEXT("Sec-WebSocket-Key"), FGuid::NewGuid().ToString(EGuidFormats::Short)); + Headers.Add(TEXT("Sec-WebSocket-Version"), TEXT("13")); + Headers.Add(TEXT("User-Agent"), TEXT("X-UnrealEngine-Agent")); + + // Unreal WebSockets are not able to do DNS resolution for localhost for some reason + // so this is using the IPv4 Loopback Address instead. + WatchWebSocket = FWebSocketsModule::Get().CreateWebSocket( + FString::Format(TEXT("ws://127.0.0.1:{0}/watch/gameserver"), {*HttpPort}), TEXT("")); + + WatchWebSocket->OnRawMessage().AddUObject(this, &UAgonesComponent::HandleWatchMessage); + } + + if (WatchWebSocket != nullptr) + { + if (!WatchWebSocket->IsConnected()) + { + WatchWebSocket->Connect(); + } + + // Only start the timer if there is a websocket to check. + // This timer has nothing to do with health and only matters if the agent is somehow + // restarted, which would be a failure condition in normal operation. + if (!EnsureWebSocketTimerHandler.IsValid()) + { + FTimerDelegate TimerDel; + TimerDel.BindUObject(this, &UAgonesComponent::EnsureWebSocketConnection); + GetWorld()->GetTimerManager().SetTimer( + EnsureWebSocketTimerHandler, TimerDel, 15.0f, true); + } + } +} + +void UAgonesComponent::WatchGameServer(const FGameServerDelegate WatchDelegate) +{ + WatchGameServerCallbacks.Add(WatchDelegate); + EnsureWebSocketConnection(); +} + + void UAgonesComponent::DeserializeAndBroadcastWatch(FString const& JsonString) +{ + TSharedRef> const JsonReader = TJsonReaderFactory::Create(JsonString); + + TSharedPtr JsonObject; + const TSharedPtr* ResultObject = nullptr; + + if (!FJsonSerializer::Deserialize(JsonReader, JsonObject) || + !JsonObject.IsValid() || + !JsonObject->TryGetObjectField(TEXT("result"), ResultObject) || + !ResultObject->IsValid()) + { + return; + } + + FGameServerResponse const Result = FGameServerResponse(*ResultObject); + for (FGameServerDelegate const& Callback : WatchGameServerCallbacks) + { + if (Callback.IsBound()) + { + Callback.Execute(Result); + } + } +} + +void UAgonesComponent::HandleWatchMessage(const void* Data, SIZE_T Size, SIZE_T BytesRemaining) +{ + if (BytesRemaining > 0) + { + WatchMessageBuffer.Append(UTF8_TO_TCHAR(static_cast(Data)), Size); + return; + } + + FString const Message = FString(Size, UTF8_TO_TCHAR(static_cast(Data))); + + // If the LHS of FString + is empty, it just uses the RHS directly so there's no copy here with an empty buffer. + DeserializeAndBroadcastWatch(WatchMessageBuffer + Message); + + // Faster to check and then empty vs blindly emptying - normal case is that the buffer is already empty + if (!WatchMessageBuffer.IsEmpty()) + { + WatchMessageBuffer.Empty(); + } +} + +void UAgonesComponent::SetLabel( + FString& Key, FString& Value, const FSetLabelDelegate SuccessDelegate, const FAgonesErrorDelegate ErrorDelegate) +{ + const FKeyValuePair Label = {Key, Value}; + FString Json; + if (!FJsonObjectConverter::UStructToJsonObjectString(Label, Json)) + { + ErrorDelegate.ExecuteIfBound({FString::Format(TEXT("error serializing key-value pair ({0}: {1}})"), {*Key, *Value})}); + return; + } + + FHttpRequestRef Request = BuildAgonesRequest("metadata/label", FHttpVerb::Put, Json); + Request->OnProcessRequestComplete().BindWeakLambda(this, + [SuccessDelegate, ErrorDelegate](FHttpRequestPtr HttpRequest, const FHttpResponsePtr HttpResponse, const bool bSucceeded) { + if (!bSucceeded) + { + ErrorDelegate.ExecuteIfBound({"Unsuccessful Call"}); + return; + } + + if (!EHttpResponseCodes::IsOk(HttpResponse->GetResponseCode())) + { + ErrorDelegate.ExecuteIfBound( + {FString::Format(TEXT("Error Code - {0}"), {FString::FromInt(HttpResponse->GetResponseCode())})}); + return; + } + + SuccessDelegate.ExecuteIfBound({}); + }); + Request->ProcessRequest(); +} + +void UAgonesComponent::Health(const FHealthDelegate SuccessDelegate, const FAgonesErrorDelegate ErrorDelegate) +{ + FHttpRequestRef Request = BuildAgonesRequest("health"); + Request->OnProcessRequestComplete().BindWeakLambda(this, + [SuccessDelegate, ErrorDelegate](FHttpRequestPtr HttpRequest, const FHttpResponsePtr HttpResponse, const bool bSucceeded) { + if (!bSucceeded) + { + ErrorDelegate.ExecuteIfBound({"Unsuccessful Call"}); + return; + } + + if (!EHttpResponseCodes::IsOk(HttpResponse->GetResponseCode())) + { + ErrorDelegate.ExecuteIfBound( + {FString::Format(TEXT("Error Code - {0}"), {FString::FromInt(HttpResponse->GetResponseCode())})}); + return; + } + + SuccessDelegate.ExecuteIfBound({}); + }); + Request->ProcessRequest(); +} + +void UAgonesComponent::Shutdown(const FShutdownDelegate SuccessDelegate, const FAgonesErrorDelegate ErrorDelegate) +{ + FHttpRequestRef Request = BuildAgonesRequest("shutdown"); + Request->OnProcessRequestComplete().BindWeakLambda(this, + [SuccessDelegate, ErrorDelegate](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, const bool bSucceeded) { + if (!bSucceeded) + { + ErrorDelegate.ExecuteIfBound({"Unsuccessful Call"}); + return; + } + if (!EHttpResponseCodes::IsOk(HttpResponse->GetResponseCode())) + { + ErrorDelegate.ExecuteIfBound( + {FString::Format(TEXT("Error Code - {0}"), {FString::FromInt(HttpResponse->GetResponseCode())})}); + return; + } + + SuccessDelegate.ExecuteIfBound({}); + }); + Request->ProcessRequest(); +} + +void UAgonesComponent::SetAnnotation( + FString& Key, FString& Value, const FSetAnnotationDelegate SuccessDelegate, const FAgonesErrorDelegate ErrorDelegate) +{ + const FKeyValuePair Label = {Key, Value}; + FString Json; + if (!FJsonObjectConverter::UStructToJsonObjectString(Label, Json)) + { + ErrorDelegate.ExecuteIfBound({FString::Format(TEXT("error serializing key-value pair ({0}: {1}})"), {*Key, *Value})}); + return; + } + + FHttpRequestRef Request = BuildAgonesRequest("metadata/annotation", FHttpVerb::Put, Json); + Request->OnProcessRequestComplete().BindWeakLambda(this, + [SuccessDelegate, ErrorDelegate](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, const bool bSucceeded) { + if (!bSucceeded) + { + ErrorDelegate.ExecuteIfBound({"Unsuccessful Call"}); + return; + } + if (!EHttpResponseCodes::IsOk(HttpResponse->GetResponseCode())) + { + ErrorDelegate.ExecuteIfBound( + {FString::Format(TEXT("Error Code - {0}"), {FString::FromInt(HttpResponse->GetResponseCode())})}); + return; + } + + SuccessDelegate.ExecuteIfBound({}); + }); + Request->ProcessRequest(); +} + +void UAgonesComponent::Allocate(const FAllocateDelegate SuccessDelegate, const FAgonesErrorDelegate ErrorDelegate) +{ + FHttpRequestRef Request = BuildAgonesRequest("allocate"); + Request->OnProcessRequestComplete().BindWeakLambda(this, + [SuccessDelegate, ErrorDelegate](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, const bool bSucceeded) { + if (!bSucceeded) + { + ErrorDelegate.ExecuteIfBound({"Unsuccessful Call"}); + return; + } + if (!EHttpResponseCodes::IsOk(HttpResponse->GetResponseCode())) + { + ErrorDelegate.ExecuteIfBound( + {FString::Format(TEXT("Error Code - {0}"), {FString::FromInt(HttpResponse->GetResponseCode())})}); + return; + } + + SuccessDelegate.ExecuteIfBound({}); + }); + Request->ProcessRequest(); +} + +void UAgonesComponent::Reserve( + const int64 Seconds, const FReserveDelegate SuccessDelegate, const FAgonesErrorDelegate ErrorDelegate) +{ + const FDuration Duration = {Seconds}; + FString Json; + if (!FJsonObjectConverter::UStructToJsonObjectString(Duration, Json)) + { + ErrorDelegate.ExecuteIfBound({TEXT("Failed to serializing request")}); + return; + } + + FHttpRequestRef Request = BuildAgonesRequest("reserve", FHttpVerb::Post, Json); + Request->OnProcessRequestComplete().BindWeakLambda(this, + [SuccessDelegate, ErrorDelegate](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, const bool bSucceeded) { + if (!bSucceeded) + { + ErrorDelegate.ExecuteIfBound({"Unsuccessful Call"}); + return; + } + if (!EHttpResponseCodes::IsOk(HttpResponse->GetResponseCode())) + { + ErrorDelegate.ExecuteIfBound( + {FString::Format(TEXT("Error Code - {0}"), {FString::FromInt(HttpResponse->GetResponseCode())})}); + return; + } + + SuccessDelegate.ExecuteIfBound({}); + }); + Request->ProcessRequest(); +} + +void UAgonesComponent::PlayerConnect( + const FString PlayerId, const FPlayerConnectDelegate SuccessDelegate, const FAgonesErrorDelegate ErrorDelegate) +{ + const FAgonesPlayer Player = {PlayerId}; + FString Json; + if (!FJsonObjectConverter::UStructToJsonObjectString(Player, Json)) + { + ErrorDelegate.ExecuteIfBound({TEXT("Failed to serializing request")}); + return; + } + + // TODO(dom) - look at JSON encoding in UE4. + Json = Json.Replace(TEXT("playerId"), TEXT("playerID")); + + FHttpRequestRef Request = BuildAgonesRequest("alpha/player/connect", FHttpVerb::Post, Json); + Request->OnProcessRequestComplete().BindWeakLambda(this, + [SuccessDelegate, ErrorDelegate](FHttpRequestPtr HttpRequest, const FHttpResponsePtr HttpResponse, const bool bSucceeded) { + if (!bSucceeded) + { + ErrorDelegate.ExecuteIfBound({"Unsuccessful Call"}); + return; + } + + if (!EHttpResponseCodes::IsOk(HttpResponse->GetResponseCode())) + { + ErrorDelegate.ExecuteIfBound( + {FString::Format(TEXT("Error Code - {0}"), {FString::FromInt(HttpResponse->GetResponseCode())})}); + return; + } + + const FString Json = HttpResponse->GetContentAsString(); + TSharedPtr JsonObject; + const TSharedRef> JsonReader = TJsonReaderFactory<>::Create(Json); + if (!FJsonSerializer::Deserialize(JsonReader, JsonObject) || !JsonObject.IsValid()) + { + ErrorDelegate.ExecuteIfBound({FString::Format(TEXT("Failed to parse response - {0}"), {*Json})}); + return; + } + + SuccessDelegate.ExecuteIfBound(FConnectedResponse(JsonObject)); + }); + Request->ProcessRequest(); +} + +void UAgonesComponent::PlayerDisconnect( + const FString PlayerId, const FPlayerDisconnectDelegate SuccessDelegate, const FAgonesErrorDelegate ErrorDelegate) +{ + const FAgonesPlayer Player = {PlayerId}; + FString Json; + if (!FJsonObjectConverter::UStructToJsonObjectString(Player, Json)) + { + ErrorDelegate.ExecuteIfBound({TEXT("Failed to serializing request")}); + return; + } + + // TODO(dom) - look at JSON encoding in UE4. + Json = Json.Replace(TEXT("playerId"), TEXT("playerID")); + + FHttpRequestRef Request = BuildAgonesRequest("alpha/player/disconnect", FHttpVerb::Post, Json); + Request->OnProcessRequestComplete().BindWeakLambda(this, + [SuccessDelegate, ErrorDelegate](FHttpRequestPtr HttpRequest, const FHttpResponsePtr HttpResponse, const bool bSucceeded) { + if (!bSucceeded) + { + ErrorDelegate.ExecuteIfBound({"Unsuccessful Call"}); + return; + } + + if (!EHttpResponseCodes::IsOk(HttpResponse->GetResponseCode())) + { + ErrorDelegate.ExecuteIfBound( + {FString::Format(TEXT("Error Code - {0}"), {FString::FromInt(HttpResponse->GetResponseCode())})}); + return; + } + + const FString Json = HttpResponse->GetContentAsString(); + TSharedPtr JsonObject; + const TSharedRef> JsonReader = TJsonReaderFactory<>::Create(Json); + if (!FJsonSerializer::Deserialize(JsonReader, JsonObject) || !JsonObject.IsValid()) + { + ErrorDelegate.ExecuteIfBound({FString::Format(TEXT("Failed to parse response - {0}"), {*Json})}); + return; + } + + SuccessDelegate.ExecuteIfBound(FDisconnectResponse(JsonObject)); + }); + Request->ProcessRequest(); +} + +void UAgonesComponent::SetPlayerCapacity( + const int64 Count, const FSetPlayerCapacityDelegate SuccessDelegate, const FAgonesErrorDelegate ErrorDelegate) +{ + const FPlayerCapacity PlayerCapacity = {Count}; + FString Json; + if (!FJsonObjectConverter::UStructToJsonObjectString(PlayerCapacity, Json)) + { + ErrorDelegate.ExecuteIfBound({TEXT("Failed to serializing request")}); + return; + } + + FHttpRequestRef Request = BuildAgonesRequest("alpha/player/capacity", FHttpVerb::Post, Json); + Request->OnProcessRequestComplete().BindWeakLambda(this, + [SuccessDelegate, ErrorDelegate](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, const bool bSucceeded) { + if (!bSucceeded) + { + ErrorDelegate.ExecuteIfBound({"Unsuccessful Call"}); + return; + } + if (!EHttpResponseCodes::IsOk(HttpResponse->GetResponseCode())) + { + ErrorDelegate.ExecuteIfBound( + {FString::Format(TEXT("Error Code - {0}"), {FString::FromInt(HttpResponse->GetResponseCode())})}); + return; + } + + SuccessDelegate.ExecuteIfBound({}); + }); + Request->ProcessRequest(); +} + +void UAgonesComponent::GetPlayerCapacity(FGetPlayerCapacityDelegate SuccessDelegate, FAgonesErrorDelegate ErrorDelegate) +{ + FHttpRequestRef Request = BuildAgonesRequest("alpha/player/capacity", FHttpVerb::Get, ""); + Request->OnProcessRequestComplete().BindWeakLambda(this, + [SuccessDelegate, ErrorDelegate](FHttpRequestPtr HttpRequest, const FHttpResponsePtr HttpResponse, const bool bSucceeded) { + if (!bSucceeded) + { + ErrorDelegate.ExecuteIfBound({"Unsuccessful Call"}); + return; + } + if (!EHttpResponseCodes::IsOk(HttpResponse->GetResponseCode())) + { + ErrorDelegate.ExecuteIfBound( + {FString::Format(TEXT("Error Code - {0}"), {FString::FromInt(HttpResponse->GetResponseCode())})}); + return; + } + + const FString Json = HttpResponse->GetContentAsString(); + TSharedPtr JsonObject; + const TSharedRef> JsonReader = TJsonReaderFactory<>::Create(Json); + if (!FJsonSerializer::Deserialize(JsonReader, JsonObject) || !JsonObject.IsValid()) + { + ErrorDelegate.ExecuteIfBound({FString::Format(TEXT("Failed to parse response - {0}"), {*Json})}); + return; + } + + SuccessDelegate.ExecuteIfBound(FCountResponse(JsonObject)); + }); + Request->ProcessRequest(); +} + +void UAgonesComponent::GetPlayerCount(FGetPlayerCountDelegate SuccessDelegate, FAgonesErrorDelegate ErrorDelegate) +{ + FHttpRequestRef Request = BuildAgonesRequest("alpha/player/count", FHttpVerb::Get, ""); + Request->OnProcessRequestComplete().BindWeakLambda(this, + [SuccessDelegate, ErrorDelegate](FHttpRequestPtr HttpRequest, const FHttpResponsePtr HttpResponse, const bool bSucceeded) { + if (!bSucceeded) + { + ErrorDelegate.ExecuteIfBound({"Unsuccessful Call"}); + return; + } + + if (!EHttpResponseCodes::IsOk(HttpResponse->GetResponseCode())) + { + ErrorDelegate.ExecuteIfBound( + {FString::Format(TEXT("Error Code - {0}"), {FString::FromInt(HttpResponse->GetResponseCode())})}); + return; + } + + const FString Json = HttpResponse->GetContentAsString(); + TSharedPtr JsonObject; + const TSharedRef> JsonReader = TJsonReaderFactory<>::Create(Json); + if (!FJsonSerializer::Deserialize(JsonReader, JsonObject) || !JsonObject.IsValid()) + { + ErrorDelegate.ExecuteIfBound({FString::Format(TEXT("Failed to parse response - {0}"), {*Json})}); + return; + } + + SuccessDelegate.ExecuteIfBound(FCountResponse(JsonObject)); + }); + Request->ProcessRequest(); +} + +void UAgonesComponent::IsPlayerConnected( + const FString PlayerId, const FIsPlayerConnectedDelegate SuccessDelegate, const FAgonesErrorDelegate ErrorDelegate) +{ + FHttpRequestRef Request = + BuildAgonesRequest(FString::Format(TEXT("alpha/player/connected/{0}"), {*PlayerId}), FHttpVerb::Get, ""); + Request->OnProcessRequestComplete().BindWeakLambda(this, + [SuccessDelegate, ErrorDelegate](FHttpRequestPtr HttpRequest, const FHttpResponsePtr HttpResponse, const bool bSucceeded) { + if (!bSucceeded) + { + ErrorDelegate.ExecuteIfBound({"Unsuccessful Call"}); + return; + } + + if (!EHttpResponseCodes::IsOk(HttpResponse->GetResponseCode())) + { + ErrorDelegate.ExecuteIfBound( + {FString::Format(TEXT("Error Code - {0}"), {FString::FromInt(HttpResponse->GetResponseCode())})}); + return; + } + + const FString Json = HttpResponse->GetContentAsString(); + TSharedPtr JsonObject; + const TSharedRef> JsonReader = TJsonReaderFactory<>::Create(Json); + if (!FJsonSerializer::Deserialize(JsonReader, JsonObject) || !JsonObject.IsValid()) + { + ErrorDelegate.ExecuteIfBound({FString::Format(TEXT("Failed to parse response - {0}"), {*Json})}); + return; + } + + SuccessDelegate.ExecuteIfBound(FConnectedResponse(JsonObject)); + }); + Request->ProcessRequest(); +} + +void UAgonesComponent::GetConnectedPlayers( + const FGetConnectedPlayersDelegate SuccessDelegate, const FAgonesErrorDelegate ErrorDelegate) +{ + FHttpRequestRef Request = BuildAgonesRequest("alpha/player/connected/{0}", FHttpVerb::Get, ""); + Request->OnProcessRequestComplete().BindWeakLambda(this, + [SuccessDelegate, ErrorDelegate](FHttpRequestPtr HttpRequest, const FHttpResponsePtr HttpResponse, const bool bSucceeded) { + if (!bSucceeded) + { + ErrorDelegate.ExecuteIfBound({"Unsuccessful Call"}); + return; + } + + if (!EHttpResponseCodes::IsOk(HttpResponse->GetResponseCode())) + { + ErrorDelegate.ExecuteIfBound( + {FString::Format(TEXT("Error Code - {0}"), {FString::FromInt(HttpResponse->GetResponseCode())})}); + return; + } + + const FString Json = HttpResponse->GetContentAsString(); + TSharedPtr JsonObject; + const TSharedRef> JsonReader = TJsonReaderFactory<>::Create(Json); + if (!FJsonSerializer::Deserialize(JsonReader, JsonObject) || !JsonObject.IsValid()) + { + ErrorDelegate.ExecuteIfBound({FString::Format(TEXT("Failed to parse response - {0}"), {*Json})}); + return; + } + + SuccessDelegate.ExecuteIfBound(FConnectedPlayersResponse(JsonObject)); + }); + Request->ProcessRequest(); +} diff --git a/examples/unrealengine/Plugins/Agones/Source/Agones/Public/Agones.h b/examples/unrealengine/Plugins/Agones/Source/Agones/Public/Agones.h new file mode 100644 index 0000000000..587b2508f0 --- /dev/null +++ b/examples/unrealengine/Plugins/Agones/Source/Agones/Public/Agones.h @@ -0,0 +1,25 @@ +// Copyright 2020 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include "Modules/ModuleManager.h" + +class FAgonesModule : public IModuleInterface +{ +public: + /** IModuleInterface implementation */ + virtual void StartupModule() override; + virtual void ShutdownModule() override; +}; diff --git a/examples/unrealengine/Plugins/Agones/Source/Agones/Public/AgonesComponent.h b/examples/unrealengine/Plugins/Agones/Source/Agones/Public/AgonesComponent.h new file mode 100644 index 0000000000..65549e984a --- /dev/null +++ b/examples/unrealengine/Plugins/Agones/Source/Agones/Public/AgonesComponent.h @@ -0,0 +1,317 @@ +// Copyright 2020 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include "Classes.h" +#include "Components/ActorComponent.h" +#include "CoreMinimal.h" +#include "Interfaces/IHttpRequest.h" +#include "WebSockets/Public/IWebSocket.h" + +#include "AgonesComponent.generated.h" + +DECLARE_DYNAMIC_DELEGATE_OneParam(FAgonesErrorDelegate, const FAgonesError&, Error); + +DECLARE_DYNAMIC_DELEGATE_OneParam(FAllocateDelegate, const FEmptyResponse&, Response); + +DECLARE_DYNAMIC_DELEGATE_OneParam(FGameServerDelegate, const FGameServerResponse&, Response); + +DECLARE_DYNAMIC_DELEGATE_OneParam(FGetConnectedPlayersDelegate, const FConnectedPlayersResponse&, Response); + +DECLARE_DYNAMIC_DELEGATE_OneParam(FGetPlayerCapacityDelegate, const FCountResponse&, Response); + +DECLARE_DYNAMIC_DELEGATE_OneParam(FGetPlayerCountDelegate, const FCountResponse&, Response); + +DECLARE_DYNAMIC_DELEGATE_OneParam(FHealthDelegate, const FEmptyResponse&, Response); + +DECLARE_DYNAMIC_DELEGATE_OneParam(FIsPlayerConnectedDelegate, const FConnectedResponse&, Response); + +DECLARE_DYNAMIC_DELEGATE_OneParam(FPlayerConnectDelegate, const FConnectedResponse&, Response); + +DECLARE_DYNAMIC_DELEGATE_OneParam(FPlayerDisconnectDelegate, const FDisconnectResponse&, Response); + +DECLARE_DYNAMIC_DELEGATE_OneParam(FReadyDelegate, const FEmptyResponse&, Response); + +DECLARE_DYNAMIC_DELEGATE_OneParam(FReserveDelegate, const FEmptyResponse&, Response); + +DECLARE_DYNAMIC_DELEGATE_OneParam(FSetAnnotationDelegate, const FEmptyResponse&, Response); + +DECLARE_DYNAMIC_DELEGATE_OneParam(FSetLabelDelegate, const FEmptyResponse&, Response); + +DECLARE_DYNAMIC_DELEGATE_OneParam(FSetPlayerCapacityDelegate, const FEmptyResponse&, Response); + +DECLARE_DYNAMIC_DELEGATE_OneParam(FShutdownDelegate, const FEmptyResponse&, Response); + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FConnectedDelegate, const FGameServerResponse&, Response); + +class FHttpVerb +{ +public: + enum EVerb + { + Get, + Post, + Put + }; + + // ReSharper disable once CppNonExplicitConvertingConstructor + FHttpVerb(const EVerb Verb) : Verb(Verb) + { + } + + FString ToString() const + { + switch (Verb) + { + case Post: + return TEXT("POST"); + case Put: + return TEXT("PUT"); + case Get: + default: + return TEXT("GET"); + } + } + +private: + const EVerb Verb; +}; + +/** + * \brief UAgonesComponent is the Unreal Component to call to the Agones SDK. + * See - https://agones.dev/ for more information. + */ +UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent), Config = Game, defaultconfig) +class AGONES_API UAgonesComponent final : public UActorComponent +{ + GENERATED_BODY() + +public: + UAgonesComponent(); + + /** + * \brief HttpPort is the default Agones HTTP port to use. + */ + UPROPERTY(EditAnywhere, Category = Agones, Config) + FString HttpPort = "9358"; + + /** + * \brief HealthRateSeconds is the frequency to send Health calls. Value of 0 will disable auto health calls. + */ + UPROPERTY(EditAnywhere, Category = Agones, Config) + float HealthRateSeconds = 10.f; + + /** + * \brief bDisableAutoConnect will stop the component auto connecting (calling GamesServer and Ready). + */ + UPROPERTY(EditAnywhere, Category = Agones, Config) + bool bDisableAutoConnect; + + /** + * \brief ConnectedDelegate will be called once the Connect func gets a successful response from GameServer. + */ + UPROPERTY(BlueprintAssignable, Category = Agones) + FConnectedDelegate ConnectedDelegate; + + /** + * \brief BeginPlay is a built in UE4 function that is called as the component is created. + */ + virtual void BeginPlay() override; + + /** + * \brief EndPlay is a built in UE4 function that is called as the component is destroyed. + * \param EndPlayReason reason for Ending Play. + */ + virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; + + /** + * \brief HealthPing loops calling the Health endpoint. + * \param RateSeconds rate at which the Health endpoint should be called. + */ + UFUNCTION(BlueprintCallable, Category = "Agones | Utility") + void HealthPing(float RateSeconds); + + /** + * \brief Connect will call /gameserver till a successful response then call /ready + * a delegate is called with the gameserver response after /ready call is made. + */ + UFUNCTION(BlueprintCallable, Category = "Agones | Utility") + void Connect(); + + /** + * \brief Allocate self marks this gameserver as Allocated. + * \param SuccessDelegate - Called on Successful call. + * \param ErrorDelegate - Called on Unsuccessful call. + */ + UFUNCTION(BlueprintCallable, Category = "Agones | Lifecycle") + void Allocate(FAllocateDelegate SuccessDelegate, FAgonesErrorDelegate ErrorDelegate); + + /** + * \brief GameServer retrieve the GameServer details. + * \param SuccessDelegate - Called on Successful call. + * \param ErrorDelegate - Called on Unsuccessful call. + */ + UFUNCTION(BlueprintCallable, Category = "Agones | Configuration") + void GameServer(FGameServerDelegate SuccessDelegate, FAgonesErrorDelegate ErrorDelegate); + + /** + * \brief WatchGameServer subscribes a delegate to be called whenever game server details change. + * \param WatchDelegate - Called every time the game server data changes. + */ + UFUNCTION(BlueprintCallable, Category = "Agones | Configuration") + void WatchGameServer(FGameServerDelegate WatchDelegate); + + /** + * \brief Health sends a ping to the health check to indicate that this server is healthy. + * \param SuccessDelegate - Called on Successful call. + * \param ErrorDelegate - Called on Unsuccessful call. + */ + UFUNCTION(BlueprintCallable, Category = "Agones | Lifecycle") + void Health(FHealthDelegate SuccessDelegate, FAgonesErrorDelegate ErrorDelegate); + + /** + * \brief Ready marks the Game Server as ready to receive connections. + * \param SuccessDelegate - Called on Successful call. + * \param ErrorDelegate - Called on Unsuccessful call. + */ + UFUNCTION(BlueprintCallable, Category = "Agones | Lifecycle") + void Ready(FReadyDelegate SuccessDelegate, FAgonesErrorDelegate ErrorDelegate); + + /** + * \brief Reserve marks the Game Server as Reserved for a given duration. + * \param Seconds - Seconds that the Game Server will be reserved. + * \param SuccessDelegate - Called on Successful call. + * \param ErrorDelegate - Called on Unsuccessful call. + */ + UFUNCTION(BlueprintCallable, Category = "Agones | Lifecycle") + void Reserve(int64 Seconds, FReserveDelegate SuccessDelegate, FAgonesErrorDelegate ErrorDelegate); + + /** + * \brief SetAnnotation sets a metadata annotation on the `GameServer` with the prefix 'agones.dev/sdk-' + * calling SetAnnotation("foo", "bar", {}, {}) will result in the annotation "agones.dev/sdk-foo: bar". + * \param Key + * \param Value + * \param SuccessDelegate - Called on Successful call. + * \param ErrorDelegate - Called on Unsuccessful call. + */ + UFUNCTION(BlueprintCallable, Category = "Agones | Metadata") + void SetAnnotation(FString& Key, FString& Value, FSetAnnotationDelegate SuccessDelegate, FAgonesErrorDelegate ErrorDelegate); + + /** + * \brief SetLabel sets a metadata label on the `GameServer` with the prefix 'agones.dev/sdk-' + * calling SetLabel("foo", "bar", {}, {}) will result in the label "agones.dev/sdk-foo: bar". + * \param Key + * \param Value + * \param SuccessDelegate - Called on Successful call. + * \param ErrorDelegate - Called on Unsuccessful call. + */ + UFUNCTION(BlueprintCallable, Category = "Agones | Metadata") + void SetLabel(FString& Key, FString& Value, FSetLabelDelegate SuccessDelegate, FAgonesErrorDelegate ErrorDelegate); + + /** + * \brief Shutdown marks the Game Server as ready to shutdown + * \param SuccessDelegate - Called on Successful call. + * \param ErrorDelegate - Called on Unsuccessful call. + */ + UFUNCTION(BlueprintCallable, Category = "Agones | Lifecycle") + void Shutdown(FShutdownDelegate SuccessDelegate, FAgonesErrorDelegate ErrorDelegate); + + /** + * \brief [Alpha] GetConnectedPlayers returns the list of the currently connected player ids. + * \param SuccessDelegate - Called on Successful call. + * \param ErrorDelegate - Called on Unsuccessful call. + */ + UFUNCTION(BlueprintCallable, Category = "Agones | Alpha | Player Tracking") + void GetConnectedPlayers(FGetConnectedPlayersDelegate SuccessDelegate, FAgonesErrorDelegate ErrorDelegate); + + /** + * \brief [Alpha] GetPlayerCapacity gets the last player capacity that was set through the SDK. + * \param SuccessDelegate - Called on Successful call. + * \param ErrorDelegate - Called on Unsuccessful call. + */ + UFUNCTION(BlueprintCallable, Category = "Agones | Alpha | Player Tracking") + void GetPlayerCapacity(FGetPlayerCapacityDelegate SuccessDelegate, FAgonesErrorDelegate ErrorDelegate); + + /** + * \brief [Alpha] GetPlayerCount returns the current player count + * \param SuccessDelegate - Called on Successful call. + * \param ErrorDelegate - Called on Unsuccessful call. + */ + UFUNCTION(BlueprintCallable, Category = "Agones | Alpha | Player Tracking") + void GetPlayerCount(FGetPlayerCountDelegate SuccessDelegate, FAgonesErrorDelegate ErrorDelegate); + + /** + * \brief [Alpha] IsPlayerConnected returns if the playerID is currently connected to the GameServer. + * \param PlayerId - PlayerID of player to check. + * \param SuccessDelegate - Called on Successful call. + * \param ErrorDelegate - Called on Unsuccessful call. + */ + UFUNCTION(BlueprintCallable, Category = "Agones | Alpha | Player Tracking") + void IsPlayerConnected(FString PlayerId, FIsPlayerConnectedDelegate SuccessDelegate, FAgonesErrorDelegate ErrorDelegate); + + /** + * \brief [Alpha] PlayerConnect increases the SDK’s stored player count by one, and appends this playerID to status.players.id. + * \param PlayerId - PlayerID of connecting player. + * \param SuccessDelegate - Called on Successful call. + * \param ErrorDelegate - Called on Unsuccessful call. + */ + UFUNCTION(BlueprintCallable, Category = "Agones | Alpha | Player Tracking") + void PlayerConnect(FString PlayerId, FPlayerConnectDelegate SuccessDelegate, FAgonesErrorDelegate ErrorDelegate); + + /** + * \brief [Alpha] PlayerDisconnect Decreases the SDK’s stored player count by one, and removes the playerID from + * status.players.id. + * + * \param PlayerId - PlayerID of disconnecting player. + * \param SuccessDelegate - Called on Successful call. + * \param ErrorDelegate - Called on Unsuccessful call. + */ + UFUNCTION(BlueprintCallable, Category = "Agones | Alpha | Player Tracking") + void PlayerDisconnect(FString PlayerId, FPlayerDisconnectDelegate SuccessDelegate, FAgonesErrorDelegate ErrorDelegate); + + /** + * \brief [Alpha] SetPlayerCapacity changes the player capacity to a new value. + * \param Count - Capacity of game server. + * \param SuccessDelegate - Called on Successful call. + * \param ErrorDelegate - Called on Unsuccessful call. + */ + UFUNCTION(BlueprintCallable, Category = "Agones | Alpha | Player Tracking") + void SetPlayerCapacity(int64 Count, FSetPlayerCapacityDelegate SuccessDelegate, FAgonesErrorDelegate ErrorDelegate); + +private: + FHttpRequestRef BuildAgonesRequest( + FString Path = "", const FHttpVerb Verb = FHttpVerb::Post, const FString Content = "{}"); + + void HandleWatchMessage(const void* Data, SIZE_T Size, SIZE_T BytesRemaining); + + void DeserializeAndBroadcastWatch(FString const& JsonString); + + void EnsureWebSocketConnection(); + + FTimerHandle ConnectDelTimerHandle; + + FTimerHandle HealthTimerHandler; + + FTimerHandle EnsureWebSocketTimerHandler; + + TSharedPtr WatchWebSocket; + + FString WatchMessageBuffer; + + TArray WatchGameServerCallbacks; + + UFUNCTION(BlueprintInternalUseOnly) + void ConnectSuccess(FGameServerResponse GameServerResponse); +}; diff --git a/examples/unrealengine/README.md b/examples/unrealengine/README.md new file mode 100644 index 0000000000..82b7e6892d --- /dev/null +++ b/examples/unrealengine/README.md @@ -0,0 +1,41 @@ +# Unreal Engine Example +This is an example unreal engine game to show how the Agones Unreal Engine plugin can interact with Agones. This example is working on Unreal Engine 2.26.2 but the general principals apply to older builds and Unreal Engine 5 going into the future. To keep file sizes as small as possible and to minimize content being reliant on versions, no compiled content has been. + +## Overview +This example contains a simple login interface which takes in an IP to connect to. The IP can be gathered from your cluster or will be your localhost in the case of purely local development. Use the following command to get the IP address of any available gameservers in the default namespace. + +``` +kubectl get gs +``` + +## Prerequisites +- Unreal Engine version of choice built from source code and working +- Agones configured and working in a kubernetes cluster + +## Getting Started +### Compiling and Packaging +1. Clone the example files to disk +2. Right-Click `AgonesExample.uproject`, click `Generate Visual Studio Project Files`, and select the correct version of Unreal Engine you will be using. + - If you do not have this option, you need to find the `Unreal Build Tool` installation directory and run + - `UnrealBuildTool.exe -projectfiles -project="Full:\Path\To\Project\AgonesExample.uproject" -game -Engine -rocket -progress` + - The unreal build tool should be in the Unreal Engine installation directory: `UnrealEngine\Engine\Binaries\DotNET\UnrealBuildTool\UnrealBuildTool.exe` +3. Open up the project solution file `AgonesExample.sln` in your editor of choice and compile the game. +4. Launch the development editor under debug mode +5. Package for project for your OS, set the deployment to Shipping, and set the target to `Server`. Select an appropriate folder for the destination. A good example would be a `./Package/Shipping` folder in directory root. +6. Package the project for your OS, set the deployment to Shipping, and set the target to `Client`. Select the same folder you chose for the previous packaging. + +You should now have a fully functional client and server. The gameserver is now reliant on the [Agones sidecar](https://agones.dev/site/docs/guides/client-sdks/local/) and will only work properly with this configured. When deploying to your Kuberentes cluster, this is handled automatically. However if you want to test locally, you will need to run the sidecar before you start the dedicated server. + +### Local Debugging +1. Run the Agones sidecar. A precompiled version can be downloaded from [here](https://agones.dev/site/docs/guides/client-sdks/local/). +2. Run the dedicated server. +3. Observe the agones sidecar and watch for ready requests, updates, and shutdowns. +### Cluster Deployment +The easiest way to get Unreal Engine working in Kuberentes is with a Linux based Docker image. There are many ways to do so, but here is one way. +1. [Configure agones in a cluster](https://agones.dev/site/docs/installation/install-agones/helm/) +2. Package the project for Linux, set the deployment to Shipping, and set the target to `Server`. Select the same folder you chose for the previous packaging. +3. Build a docker image from the packaged project. An example dockerfile [server.Dockerfile](server.Dockerfile) has been given. Be sure to edit it to point to the location of your packaged project. +4. Push the docker image to a public facing docker repository +5. Configure an Agones [GameServer Specification](https://agones.dev/site/docs/reference/gameserver/). Another template more specific to Unreal has been given under [gameserver.yaml](gameserver.yaml). +6. Deploy the gameserver to your cluster + diff --git a/examples/unrealengine/Source/AgonesExample.Target.cs b/examples/unrealengine/Source/AgonesExample.Target.cs new file mode 100644 index 0000000000..04dd2f06f6 --- /dev/null +++ b/examples/unrealengine/Source/AgonesExample.Target.cs @@ -0,0 +1,14 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +using UnrealBuildTool; +using System.Collections.Generic; + +public class AgonesExampleTarget : TargetRules +{ + public AgonesExampleTarget(TargetInfo Target) : base(Target) + { + Type = TargetType.Game; + DefaultBuildSettings = BuildSettingsVersion.V2; + ExtraModuleNames.Add("AgonesExample"); + } +} diff --git a/examples/unrealengine/Source/AgonesExample/AgonesExample.Build.cs b/examples/unrealengine/Source/AgonesExample/AgonesExample.Build.cs new file mode 100644 index 0000000000..533f21a367 --- /dev/null +++ b/examples/unrealengine/Source/AgonesExample/AgonesExample.Build.cs @@ -0,0 +1,22 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +using UnrealBuildTool; + +public class AgonesExample : ModuleRules +{ + public AgonesExample(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; + + PublicDependencyModuleNames.AddRange( + new string[] + { + "Agones", + "Core", + "CoreUObject", + "Engine", + "InputCore", + "HeadMountedDisplay" + }); + } +} diff --git a/examples/unrealengine/Source/AgonesExample/AgonesExample.cpp b/examples/unrealengine/Source/AgonesExample/AgonesExample.cpp new file mode 100644 index 0000000000..48eb8fcb25 --- /dev/null +++ b/examples/unrealengine/Source/AgonesExample/AgonesExample.cpp @@ -0,0 +1,7 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "AgonesExample.h" +#include "Modules/ModuleManager.h" + +IMPLEMENT_PRIMARY_GAME_MODULE( FDefaultGameModuleImpl, AgonesExample, "AgonesExample" ); + \ No newline at end of file diff --git a/examples/unrealengine/Source/AgonesExample/AgonesExample.h b/examples/unrealengine/Source/AgonesExample/AgonesExample.h new file mode 100644 index 0000000000..8e482a2801 --- /dev/null +++ b/examples/unrealengine/Source/AgonesExample/AgonesExample.h @@ -0,0 +1,5 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" diff --git a/examples/unrealengine/Source/AgonesExample/AgonesExampleCharacter.cpp b/examples/unrealengine/Source/AgonesExample/AgonesExampleCharacter.cpp new file mode 100644 index 0000000000..ba568130ef --- /dev/null +++ b/examples/unrealengine/Source/AgonesExample/AgonesExampleCharacter.cpp @@ -0,0 +1,300 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "AgonesExampleCharacter.h" +#include "AgonesExampleProjectile.h" +#include "Animation/AnimInstance.h" +#include "Camera/CameraComponent.h" +#include "Components/CapsuleComponent.h" +#include "Components/InputComponent.h" +#include "GameFramework/InputSettings.h" +#include "HeadMountedDisplayFunctionLibrary.h" +#include "Kismet/GameplayStatics.h" +#include "MotionControllerComponent.h" +#include "XRMotionControllerBase.h" // for FXRMotionControllerBase::RightHandSourceId + +DEFINE_LOG_CATEGORY_STATIC(LogFPChar, Warning, All); + +////////////////////////////////////////////////////////////////////////// +// AAgonesExampleCharacter + +AAgonesExampleCharacter::AAgonesExampleCharacter() +{ + // Set size for collision capsule + GetCapsuleComponent()->InitCapsuleSize(55.f, 96.0f); + + // set our turn rates for input + BaseTurnRate = 45.f; + BaseLookUpRate = 45.f; + + // Create a CameraComponent + FirstPersonCameraComponent = CreateDefaultSubobject(TEXT("FirstPersonCamera")); + FirstPersonCameraComponent->SetupAttachment(GetCapsuleComponent()); + FirstPersonCameraComponent->SetRelativeLocation(FVector(-39.56f, 1.75f, 64.f)); // Position the camera + FirstPersonCameraComponent->bUsePawnControlRotation = true; + + // Create a mesh component that will be used when being viewed from a '1st person' view (when controlling this pawn) + Mesh1P = CreateDefaultSubobject(TEXT("CharacterMesh1P")); + Mesh1P->SetOnlyOwnerSee(true); + Mesh1P->SetupAttachment(FirstPersonCameraComponent); + Mesh1P->bCastDynamicShadow = false; + Mesh1P->CastShadow = false; + Mesh1P->SetRelativeRotation(FRotator(1.9f, -19.19f, 5.2f)); + Mesh1P->SetRelativeLocation(FVector(-0.5f, -4.4f, -155.7f)); + + // Create a gun mesh component + FP_Gun = CreateDefaultSubobject(TEXT("FP_Gun")); + FP_Gun->SetOnlyOwnerSee(false); // otherwise won't be visible in the multiplayer + FP_Gun->bCastDynamicShadow = false; + FP_Gun->CastShadow = false; + // FP_Gun->SetupAttachment(Mesh1P, TEXT("GripPoint")); + FP_Gun->SetupAttachment(RootComponent); + + FP_MuzzleLocation = CreateDefaultSubobject(TEXT("MuzzleLocation")); + FP_MuzzleLocation->SetupAttachment(FP_Gun); + FP_MuzzleLocation->SetRelativeLocation(FVector(0.2f, 48.4f, -10.6f)); + + // Default offset from the character location for projectiles to spawn + GunOffset = FVector(100.0f, 0.0f, 10.0f); + + // Note: The ProjectileClass and the skeletal mesh/anim blueprints for Mesh1P, FP_Gun, and VR_Gun + // are set in the derived blueprint asset named MyCharacter to avoid direct content references in C++. + + // Create VR Controllers. + R_MotionController = CreateDefaultSubobject(TEXT("R_MotionController")); + R_MotionController->MotionSource = FXRMotionControllerBase::RightHandSourceId; + R_MotionController->SetupAttachment(RootComponent); + L_MotionController = CreateDefaultSubobject(TEXT("L_MotionController")); + L_MotionController->SetupAttachment(RootComponent); + + // Create a gun and attach it to the right-hand VR controller. + // Create a gun mesh component + VR_Gun = CreateDefaultSubobject(TEXT("VR_Gun")); + VR_Gun->SetOnlyOwnerSee(false); // otherwise won't be visible in the multiplayer + VR_Gun->bCastDynamicShadow = false; + VR_Gun->CastShadow = false; + VR_Gun->SetupAttachment(R_MotionController); + VR_Gun->SetRelativeRotation(FRotator(0.0f, -90.0f, 0.0f)); + + VR_MuzzleLocation = CreateDefaultSubobject(TEXT("VR_MuzzleLocation")); + VR_MuzzleLocation->SetupAttachment(VR_Gun); + VR_MuzzleLocation->SetRelativeLocation(FVector(0.000004, 53.999992, 10.000000)); + VR_MuzzleLocation->SetRelativeRotation(FRotator(0.0f, 90.0f, 0.0f)); // Counteract the rotation of the VR gun model. + + // Uncomment the following line to turn motion controllers on by default: + //bUsingMotionControllers = true; +} + +void AAgonesExampleCharacter::BeginPlay() +{ + // Call the base class + Super::BeginPlay(); + + //Attach gun mesh component to Skeleton, doing it here because the skeleton is not yet created in the constructor + FP_Gun->AttachToComponent(Mesh1P, FAttachmentTransformRules(EAttachmentRule::SnapToTarget, true), TEXT("GripPoint")); + + // Show or hide the two versions of the gun based on whether or not we're using motion controllers. + if (bUsingMotionControllers) + { + VR_Gun->SetHiddenInGame(false, true); + Mesh1P->SetHiddenInGame(true, true); + } + else + { + VR_Gun->SetHiddenInGame(true, true); + Mesh1P->SetHiddenInGame(false, true); + } +} + +////////////////////////////////////////////////////////////////////////// +// Input + +void AAgonesExampleCharacter::SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) +{ + // set up gameplay key bindings + check(PlayerInputComponent); + + // Bind jump events + PlayerInputComponent->BindAction("Jump", IE_Pressed, this, &ACharacter::Jump); + PlayerInputComponent->BindAction("Jump", IE_Released, this, &ACharacter::StopJumping); + + // Bind fire event + PlayerInputComponent->BindAction("Fire", IE_Pressed, this, &AAgonesExampleCharacter::OnFire); + + // Enable touchscreen input + EnableTouchscreenMovement(PlayerInputComponent); + + PlayerInputComponent->BindAction("ResetVR", IE_Pressed, this, &AAgonesExampleCharacter::OnResetVR); + + // Bind movement events + PlayerInputComponent->BindAxis("MoveForward", this, &AAgonesExampleCharacter::MoveForward); + PlayerInputComponent->BindAxis("MoveRight", this, &AAgonesExampleCharacter::MoveRight); + + // We have 2 versions of the rotation bindings to handle different kinds of devices differently + // "turn" handles devices that provide an absolute delta, such as a mouse. + // "turnrate" is for devices that we choose to treat as a rate of change, such as an analog joystick + PlayerInputComponent->BindAxis("Turn", this, &APawn::AddControllerYawInput); + PlayerInputComponent->BindAxis("TurnRate", this, &AAgonesExampleCharacter::TurnAtRate); + PlayerInputComponent->BindAxis("LookUp", this, &APawn::AddControllerPitchInput); + PlayerInputComponent->BindAxis("LookUpRate", this, &AAgonesExampleCharacter::LookUpAtRate); +} + +void AAgonesExampleCharacter::OnFire() +{ + // try and fire a projectile + if (ProjectileClass != nullptr) + { + UWorld* const World = GetWorld(); + if (World != nullptr) + { + if (bUsingMotionControllers) + { + const FRotator SpawnRotation = VR_MuzzleLocation->GetComponentRotation(); + const FVector SpawnLocation = VR_MuzzleLocation->GetComponentLocation(); + World->SpawnActor(ProjectileClass, SpawnLocation, SpawnRotation); + } + else + { + const FRotator SpawnRotation = GetControlRotation(); + // MuzzleOffset is in camera space, so transform it to world space before offsetting from the character location to find the final muzzle position + const FVector SpawnLocation = ((FP_MuzzleLocation != nullptr) ? FP_MuzzleLocation->GetComponentLocation() : GetActorLocation()) + SpawnRotation.RotateVector(GunOffset); + + //Set Spawn Collision Handling Override + FActorSpawnParameters ActorSpawnParams; + ActorSpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButDontSpawnIfColliding; + + // spawn the projectile at the muzzle + World->SpawnActor(ProjectileClass, SpawnLocation, SpawnRotation, ActorSpawnParams); + } + } + } + + // try and play the sound if specified + if (FireSound != nullptr) + { + UGameplayStatics::PlaySoundAtLocation(this, FireSound, GetActorLocation()); + } + + // try and play a firing animation if specified + if (FireAnimation != nullptr) + { + // Get the animation object for the arms mesh + UAnimInstance* AnimInstance = Mesh1P->GetAnimInstance(); + if (AnimInstance != nullptr) + { + AnimInstance->Montage_Play(FireAnimation, 1.f); + } + } +} + +void AAgonesExampleCharacter::OnResetVR() +{ + UHeadMountedDisplayFunctionLibrary::ResetOrientationAndPosition(); +} + +void AAgonesExampleCharacter::BeginTouch(const ETouchIndex::Type FingerIndex, const FVector Location) +{ + if (TouchItem.bIsPressed == true) + { + return; + } + if ((FingerIndex == TouchItem.FingerIndex) && (TouchItem.bMoved == false)) + { + OnFire(); + } + TouchItem.bIsPressed = true; + TouchItem.FingerIndex = FingerIndex; + TouchItem.Location = Location; + TouchItem.bMoved = false; +} + +void AAgonesExampleCharacter::EndTouch(const ETouchIndex::Type FingerIndex, const FVector Location) +{ + if (TouchItem.bIsPressed == false) + { + return; + } + TouchItem.bIsPressed = false; +} + +//Commenting this section out to be consistent with FPS BP template. +//This allows the user to turn without using the right virtual joystick + +//void AAgonesExampleCharacter::TouchUpdate(const ETouchIndex::Type FingerIndex, const FVector Location) +//{ +// if ((TouchItem.bIsPressed == true) && (TouchItem.FingerIndex == FingerIndex)) +// { +// if (TouchItem.bIsPressed) +// { +// if (GetWorld() != nullptr) +// { +// UGameViewportClient* ViewportClient = GetWorld()->GetGameViewport(); +// if (ViewportClient != nullptr) +// { +// FVector MoveDelta = Location - TouchItem.Location; +// FVector2D ScreenSize; +// ViewportClient->GetViewportSize(ScreenSize); +// FVector2D ScaledDelta = FVector2D(MoveDelta.X, MoveDelta.Y) / ScreenSize; +// if (FMath::Abs(ScaledDelta.X) >= 4.0 / ScreenSize.X) +// { +// TouchItem.bMoved = true; +// float Value = ScaledDelta.X * BaseTurnRate; +// AddControllerYawInput(Value); +// } +// if (FMath::Abs(ScaledDelta.Y) >= 4.0 / ScreenSize.Y) +// { +// TouchItem.bMoved = true; +// float Value = ScaledDelta.Y * BaseTurnRate; +// AddControllerPitchInput(Value); +// } +// TouchItem.Location = Location; +// } +// TouchItem.Location = Location; +// } +// } +// } +//} + +void AAgonesExampleCharacter::MoveForward(float Value) +{ + if (Value != 0.0f) + { + // add movement in that direction + AddMovementInput(GetActorForwardVector(), Value); + } +} + +void AAgonesExampleCharacter::MoveRight(float Value) +{ + if (Value != 0.0f) + { + // add movement in that direction + AddMovementInput(GetActorRightVector(), Value); + } +} + +void AAgonesExampleCharacter::TurnAtRate(float Rate) +{ + // calculate delta for this frame from the rate information + AddControllerYawInput(Rate * BaseTurnRate * GetWorld()->GetDeltaSeconds()); +} + +void AAgonesExampleCharacter::LookUpAtRate(float Rate) +{ + // calculate delta for this frame from the rate information + AddControllerPitchInput(Rate * BaseLookUpRate * GetWorld()->GetDeltaSeconds()); +} + +bool AAgonesExampleCharacter::EnableTouchscreenMovement(class UInputComponent* PlayerInputComponent) +{ + if (FPlatformMisc::SupportsTouchInput() || GetDefault()->bUseMouseForTouch) + { + PlayerInputComponent->BindTouch(EInputEvent::IE_Pressed, this, &AAgonesExampleCharacter::BeginTouch); + PlayerInputComponent->BindTouch(EInputEvent::IE_Released, this, &AAgonesExampleCharacter::EndTouch); + + //Commenting this out to be more consistent with FPS BP template. + //PlayerInputComponent->BindTouch(EInputEvent::IE_Repeat, this, &AAgonesExampleCharacter::TouchUpdate); + return true; + } + + return false; +} diff --git a/examples/unrealengine/Source/AgonesExample/AgonesExampleCharacter.h b/examples/unrealengine/Source/AgonesExample/AgonesExampleCharacter.h new file mode 100644 index 0000000000..ea0cfa2388 --- /dev/null +++ b/examples/unrealengine/Source/AgonesExample/AgonesExampleCharacter.h @@ -0,0 +1,148 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/Character.h" +#include "AgonesExampleCharacter.generated.h" + +class UInputComponent; +class USkeletalMeshComponent; +class USceneComponent; +class UCameraComponent; +class UMotionControllerComponent; +class UAnimMontage; +class USoundBase; + +UCLASS(config=Game) +class AAgonesExampleCharacter : public ACharacter +{ + GENERATED_BODY() + + /** Pawn mesh: 1st person view (arms; seen only by self) */ + UPROPERTY(VisibleDefaultsOnly, Category=Mesh) + USkeletalMeshComponent* Mesh1P; + + /** Gun mesh: 1st person view (seen only by self) */ + UPROPERTY(VisibleDefaultsOnly, Category = Mesh) + USkeletalMeshComponent* FP_Gun; + + /** Location on gun mesh where projectiles should spawn. */ + UPROPERTY(VisibleDefaultsOnly, Category = Mesh) + USceneComponent* FP_MuzzleLocation; + + /** Gun mesh: VR view (attached to the VR controller directly, no arm, just the actual gun) */ + UPROPERTY(VisibleDefaultsOnly, Category = Mesh) + USkeletalMeshComponent* VR_Gun; + + /** Location on VR gun mesh where projectiles should spawn. */ + UPROPERTY(VisibleDefaultsOnly, Category = Mesh) + USceneComponent* VR_MuzzleLocation; + + /** First person camera */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true")) + UCameraComponent* FirstPersonCameraComponent; + + /** Motion controller (right hand) */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, meta = (AllowPrivateAccess = "true")) + UMotionControllerComponent* R_MotionController; + + /** Motion controller (left hand) */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, meta = (AllowPrivateAccess = "true")) + UMotionControllerComponent* L_MotionController; + +public: + AAgonesExampleCharacter(); + +protected: + virtual void BeginPlay(); + +public: + /** Base turn rate, in deg/sec. Other scaling may affect final turn rate. */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category=Camera) + float BaseTurnRate; + + /** Base look up/down rate, in deg/sec. Other scaling may affect final rate. */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category=Camera) + float BaseLookUpRate; + + /** Gun muzzle's offset from the characters location */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Gameplay) + FVector GunOffset; + + /** Projectile class to spawn */ + UPROPERTY(EditDefaultsOnly, Category=Projectile) + TSubclassOf ProjectileClass; + + /** Sound to play each time we fire */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Gameplay) + USoundBase* FireSound; + + /** AnimMontage to play each time we fire */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Gameplay) + UAnimMontage* FireAnimation; + + /** Whether to use motion controller location for aiming. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Gameplay) + uint8 bUsingMotionControllers : 1; + +protected: + + /** Fires a projectile. */ + void OnFire(); + + /** Resets HMD orientation and position in VR. */ + void OnResetVR(); + + /** Handles moving forward/backward */ + void MoveForward(float Val); + + /** Handles stafing movement, left and right */ + void MoveRight(float Val); + + /** + * Called via input to turn at a given rate. + * @param Rate This is a normalized rate, i.e. 1.0 means 100% of desired turn rate + */ + void TurnAtRate(float Rate); + + /** + * Called via input to turn look up/down at a given rate. + * @param Rate This is a normalized rate, i.e. 1.0 means 100% of desired turn rate + */ + void LookUpAtRate(float Rate); + + struct TouchData + { + TouchData() { bIsPressed = false;Location=FVector::ZeroVector;} + bool bIsPressed; + ETouchIndex::Type FingerIndex; + FVector Location; + bool bMoved; + }; + void BeginTouch(const ETouchIndex::Type FingerIndex, const FVector Location); + void EndTouch(const ETouchIndex::Type FingerIndex, const FVector Location); + void TouchUpdate(const ETouchIndex::Type FingerIndex, const FVector Location); + TouchData TouchItem; + +protected: + // APawn interface + virtual void SetupPlayerInputComponent(UInputComponent* InputComponent) override; + // End of APawn interface + + /* + * Configures input for touchscreen devices if there is a valid touch interface for doing so + * + * @param InputComponent The input component pointer to bind controls to + * @returns true if touch controls were enabled. + */ + bool EnableTouchscreenMovement(UInputComponent* InputComponent); + +public: + /** Returns Mesh1P subobject **/ + USkeletalMeshComponent* GetMesh1P() const { return Mesh1P; } + /** Returns FirstPersonCameraComponent subobject **/ + UCameraComponent* GetFirstPersonCameraComponent() const { return FirstPersonCameraComponent; } + +}; + diff --git a/examples/unrealengine/Source/AgonesExample/AgonesExampleGameMode.cpp b/examples/unrealengine/Source/AgonesExample/AgonesExampleGameMode.cpp new file mode 100644 index 0000000000..a8fae0137f --- /dev/null +++ b/examples/unrealengine/Source/AgonesExample/AgonesExampleGameMode.cpp @@ -0,0 +1,22 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "AgonesExampleGameMode.h" +#include "AgonesExampleHUD.h" +#include "AgonesExampleCharacter.h" +#include "AgonesExampleGameSession.h" +#include "UObject/ConstructorHelpers.h" + +AAgonesExampleGameMode::AAgonesExampleGameMode() + : Super() +{ + // set default pawn class to our Blueprinted character + static ConstructorHelpers::FClassFinder PlayerPawnClassFinder(TEXT("/Game/FirstPersonCPP/Blueprints/FirstPersonCharacter")); + DefaultPawnClass = PlayerPawnClassFinder.Class; + + // use our custom HUD class + HUDClass = AAgonesExampleHUD::StaticClass(); + + GameSessionClass = AAgonesExampleGameSession::StaticClass(); + + AgonesSDK = CreateDefaultSubobject(TEXT("AgonesSDK")); +} \ No newline at end of file diff --git a/examples/unrealengine/Source/AgonesExample/AgonesExampleGameMode.h b/examples/unrealengine/Source/AgonesExample/AgonesExampleGameMode.h new file mode 100644 index 0000000000..76c485ff65 --- /dev/null +++ b/examples/unrealengine/Source/AgonesExample/AgonesExampleGameMode.h @@ -0,0 +1,21 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" + +#include "AgonesComponent.h" +#include "GameFramework/GameModeBase.h" +#include "AgonesExampleGameMode.generated.h" + +UCLASS(minimalapi) +class AAgonesExampleGameMode : public AGameModeBase +{ + GENERATED_BODY() + +public: + UPROPERTY(EditAnywhere, BlueprintReadWrite) + UAgonesComponent* AgonesSDK; + + AAgonesExampleGameMode(); +}; \ No newline at end of file diff --git a/examples/unrealengine/Source/AgonesExample/AgonesExampleGameSession.cpp b/examples/unrealengine/Source/AgonesExample/AgonesExampleGameSession.cpp new file mode 100644 index 0000000000..e44ae28d48 --- /dev/null +++ b/examples/unrealengine/Source/AgonesExample/AgonesExampleGameSession.cpp @@ -0,0 +1,54 @@ +// Fill out your copyright notice in the Description page of Project Settings. + + +#include "AgonesExampleGameSession.h" + +#include "AgonesExampleGameMode.h" +#include "Kismet/GameplayStatics.h" + +AAgonesExampleGameSession::AAgonesExampleGameSession() +{ + AAgonesExampleGameMode* GameMode = Cast(UGameplayStatics::GetGameMode(GetWorld())); + + if (GameMode) + { + AgonesSDK = GameMode->AgonesSDK; + } +} + +void AAgonesExampleGameSession::RegisterServer() +{ + AgonesSDK->SetPlayerCapacity(100, {}, AgonesErrorDelegate); + FString Label = "map"; + FString Value = GetWorld()->GetCurrentLevel()->GetName(); + AgonesSDK->SetLabel(Label, Value, {}, AgonesErrorDelegate); +} + +void AAgonesExampleGameSession::PostLogin(APlayerController* NewPlayer) +{ + if (NewPlayer->IsLocalController()) + { + return; + } + + AgonesSDK->PlayerConnect(NewPlayer->GetNetConnection()->PlayerId.ToString(), PlayerConnectDelegate, AgonesErrorDelegate); +} + +void AAgonesExampleGameSession::NotifyLogout(const APlayerController* PC) +{ + if (PC->IsLocalController()) + { + return; + } + AgonesSDK->PlayerDisconnect(PC->GetNetConnection()->PlayerId.ToString(), {}, AgonesErrorDelegate); +} + +void AAgonesExampleGameSession::OnAgonesSuccessful(const FConnectedResponse& Response) +{ + UE_LOG(LogTemp, Verbose, TEXT("Agones player connection succcessful!")); +} + +void AAgonesExampleGameSession::OnAgonesError(const FAgonesError& Error) +{ + UE_LOG(LogTemp, Error, TEXT("Agones Error: %s"), *(Error.ErrorMessage)); +} diff --git a/examples/unrealengine/Source/AgonesExample/AgonesExampleGameSession.h b/examples/unrealengine/Source/AgonesExample/AgonesExampleGameSession.h new file mode 100644 index 0000000000..e74acb2512 --- /dev/null +++ b/examples/unrealengine/Source/AgonesExample/AgonesExampleGameSession.h @@ -0,0 +1,45 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/GameSession.h" +#include "AgonesComponent.h" +#include "AgonesExampleGameSession.generated.h" + +/** + * + */ +UCLASS() +class AGONESEXAMPLE_API AAgonesExampleGameSession : public AGameSession +{ + GENERATED_BODY() + +protected: + UPROPERTY(EditAnywhere, BlueprintReadWrite) + UAgonesComponent* AgonesSDK; + +public: + AAgonesExampleGameSession(); + + UPROPERTY() + FPlayerConnectDelegate PlayerConnectDelegate; + + UPROPERTY() + FAgonesErrorDelegate AgonesErrorDelegate; + + UFUNCTION() + virtual void RegisterServer() override; + + UFUNCTION() + virtual void PostLogin(APlayerController* NewPlayer) override; + + UFUNCTION() + virtual void NotifyLogout(const APlayerController* PC) override; + + UFUNCTION() + void OnAgonesSuccessful(const FConnectedResponse& Response); + + UFUNCTION() + void OnAgonesError(const FAgonesError& Error); +}; diff --git a/examples/unrealengine/Source/AgonesExample/AgonesExampleHUD.cpp b/examples/unrealengine/Source/AgonesExample/AgonesExampleHUD.cpp new file mode 100644 index 0000000000..4c9c909788 --- /dev/null +++ b/examples/unrealengine/Source/AgonesExample/AgonesExampleHUD.cpp @@ -0,0 +1,35 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "AgonesExampleHUD.h" +#include "Engine/Canvas.h" +#include "Engine/Texture2D.h" +#include "TextureResource.h" +#include "CanvasItem.h" +#include "UObject/ConstructorHelpers.h" + +AAgonesExampleHUD::AAgonesExampleHUD() +{ + // Set the crosshair texture + static ConstructorHelpers::FObjectFinder CrosshairTexObj(TEXT("/Game/FirstPerson/Textures/FirstPersonCrosshair")); + CrosshairTex = CrosshairTexObj.Object; +} + + +void AAgonesExampleHUD::DrawHUD() +{ + Super::DrawHUD(); + + // Draw very simple crosshair + + // find center of the Canvas + const FVector2D Center(Canvas->ClipX * 0.5f, Canvas->ClipY * 0.5f); + + // offset by half the texture's dimensions so that the center of the texture aligns with the center of the Canvas + const FVector2D CrosshairDrawPosition( (Center.X), + (Center.Y + 20.0f)); + + // draw the crosshair + FCanvasTileItem TileItem( CrosshairDrawPosition, CrosshairTex->Resource, FLinearColor::White); + TileItem.BlendMode = SE_BLEND_Translucent; + Canvas->DrawItem( TileItem ); +} diff --git a/examples/unrealengine/Source/AgonesExample/AgonesExampleHUD.h b/examples/unrealengine/Source/AgonesExample/AgonesExampleHUD.h new file mode 100644 index 0000000000..0b5e4eda21 --- /dev/null +++ b/examples/unrealengine/Source/AgonesExample/AgonesExampleHUD.h @@ -0,0 +1,25 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/HUD.h" +#include "AgonesExampleHUD.generated.h" + +UCLASS() +class AAgonesExampleHUD : public AHUD +{ + GENERATED_BODY() + +public: + AAgonesExampleHUD(); + + /** Primary draw call for the HUD */ + virtual void DrawHUD() override; + +private: + /** Crosshair asset pointer */ + class UTexture2D* CrosshairTex; + +}; + diff --git a/examples/unrealengine/Source/AgonesExample/AgonesExampleProjectile.cpp b/examples/unrealengine/Source/AgonesExample/AgonesExampleProjectile.cpp new file mode 100644 index 0000000000..b61a2667bc --- /dev/null +++ b/examples/unrealengine/Source/AgonesExample/AgonesExampleProjectile.cpp @@ -0,0 +1,43 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "AgonesExampleProjectile.h" +#include "GameFramework/ProjectileMovementComponent.h" +#include "Components/SphereComponent.h" + +AAgonesExampleProjectile::AAgonesExampleProjectile() +{ + // Use a sphere as a simple collision representation + CollisionComp = CreateDefaultSubobject(TEXT("SphereComp")); + CollisionComp->InitSphereRadius(5.0f); + CollisionComp->BodyInstance.SetCollisionProfileName("Projectile"); + CollisionComp->OnComponentHit.AddDynamic(this, &AAgonesExampleProjectile::OnHit); // set up a notification for when this component hits something blocking + + // Players can't walk on it + CollisionComp->SetWalkableSlopeOverride(FWalkableSlopeOverride(WalkableSlope_Unwalkable, 0.f)); + CollisionComp->CanCharacterStepUpOn = ECB_No; + + // Set as root component + RootComponent = CollisionComp; + + // Use a ProjectileMovementComponent to govern this projectile's movement + ProjectileMovement = CreateDefaultSubobject(TEXT("ProjectileComp")); + ProjectileMovement->UpdatedComponent = CollisionComp; + ProjectileMovement->InitialSpeed = 3000.f; + ProjectileMovement->MaxSpeed = 3000.f; + ProjectileMovement->bRotationFollowsVelocity = true; + ProjectileMovement->bShouldBounce = true; + + // Die after 3 seconds by default + InitialLifeSpan = 3.0f; +} + +void AAgonesExampleProjectile::OnHit(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit) +{ + // Only add impulse and destroy projectile if we hit a physics + if ((OtherActor != nullptr) && (OtherActor != this) && (OtherComp != nullptr) && OtherComp->IsSimulatingPhysics()) + { + OtherComp->AddImpulseAtLocation(GetVelocity() * 100.0f, GetActorLocation()); + + Destroy(); + } +} \ No newline at end of file diff --git a/examples/unrealengine/Source/AgonesExample/AgonesExampleProjectile.h b/examples/unrealengine/Source/AgonesExample/AgonesExampleProjectile.h new file mode 100644 index 0000000000..eca8c38918 --- /dev/null +++ b/examples/unrealengine/Source/AgonesExample/AgonesExampleProjectile.h @@ -0,0 +1,37 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/Actor.h" +#include "AgonesExampleProjectile.generated.h" + +class USphereComponent; +class UProjectileMovementComponent; + +UCLASS(config=Game) +class AAgonesExampleProjectile : public AActor +{ + GENERATED_BODY() + + /** Sphere collision component */ + UPROPERTY(VisibleDefaultsOnly, Category=Projectile) + USphereComponent* CollisionComp; + + /** Projectile movement component */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Movement, meta = (AllowPrivateAccess = "true")) + UProjectileMovementComponent* ProjectileMovement; + +public: + AAgonesExampleProjectile(); + + /** called when projectile hits something */ + UFUNCTION() + void OnHit(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit); + + /** Returns CollisionComp subobject **/ + USphereComponent* GetCollisionComp() const { return CollisionComp; } + /** Returns ProjectileMovement subobject **/ + UProjectileMovementComponent* GetProjectileMovement() const { return ProjectileMovement; } +}; + diff --git a/examples/unrealengine/Source/AgonesExampleClient.Target.cs b/examples/unrealengine/Source/AgonesExampleClient.Target.cs new file mode 100644 index 0000000000..4920f8c387 --- /dev/null +++ b/examples/unrealengine/Source/AgonesExampleClient.Target.cs @@ -0,0 +1,14 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +using UnrealBuildTool; +using System.Collections.Generic; + +public class AgonesExampleClientTarget : TargetRules +{ + public AgonesExampleClientTarget(TargetInfo Target) : base(Target) + { + Type = TargetType.Client; + DefaultBuildSettings = BuildSettingsVersion.V2; + ExtraModuleNames.Add("AgonesExample"); + } +} diff --git a/examples/unrealengine/Source/AgonesExampleEditor.Target.cs b/examples/unrealengine/Source/AgonesExampleEditor.Target.cs new file mode 100644 index 0000000000..c566475b36 --- /dev/null +++ b/examples/unrealengine/Source/AgonesExampleEditor.Target.cs @@ -0,0 +1,14 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +using UnrealBuildTool; +using System.Collections.Generic; + +public class AgonesExampleEditorTarget : TargetRules +{ + public AgonesExampleEditorTarget(TargetInfo Target) : base(Target) + { + Type = TargetType.Editor; + DefaultBuildSettings = BuildSettingsVersion.V2; + ExtraModuleNames.Add("AgonesExample"); + } +} diff --git a/examples/unrealengine/Source/AgonesExampleServer.Target.cs b/examples/unrealengine/Source/AgonesExampleServer.Target.cs new file mode 100644 index 0000000000..e298882b88 --- /dev/null +++ b/examples/unrealengine/Source/AgonesExampleServer.Target.cs @@ -0,0 +1,14 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +using UnrealBuildTool; +using System.Collections.Generic; + +public class AgonesExampleServerTarget : TargetRules +{ + public AgonesExampleServerTarget(TargetInfo Target) : base(Target) + { + Type = TargetType.Server; + DefaultBuildSettings = BuildSettingsVersion.V2; + ExtraModuleNames.Add("AgonesExample"); + } +} diff --git a/examples/unrealengine/gameserver.yaml b/examples/unrealengine/gameserver.yaml new file mode 100644 index 0000000000..e8363d9fef --- /dev/null +++ b/examples/unrealengine/gameserver.yaml @@ -0,0 +1,18 @@ +apiVersion: "agones.dev/v1" +kind: GameServer +metadata: + generateName: "unrealexample-" +spec: + ports: + - name: default + portPolicy: Dynamic + containerPort: 7777 + health: + disabled: false + periodSeconds: 10 + failureThreshold: 3 + initialDelaySeconds: 60 + spec: + containers: + - name: unrealexample + image: \ No newline at end of file diff --git a/examples/unrealengine/server.Dockerfile b/examples/unrealengine/server.Dockerfile new file mode 100644 index 0000000000..fd1ff111d8 --- /dev/null +++ b/examples/unrealengine/server.Dockerfile @@ -0,0 +1,16 @@ + +FROM debian:latest + +RUN apt-get update \ + && apt-get upgrade -y + +RUN adduser server +COPY ./Package/Shipping/LinuxServer /home/server/server +RUN chown -R server:server /home/server +RUN chmod o+x /home/server/server + +USER server + +EXPOSE 7777/udp + +ENTRYPOINT ["/home/server/server/AgonesExampleServer.sh"] \ No newline at end of file