基础游戏快速入门
概述
如果您是个新手,那么新建一个游戏项目并运行,这个过程对您来说是极为困难的。您可能会苦于不知从何处入手,到哪里应该做些什么操作等等。设计该文档的目的就是要快速突出强调新建游戏需要掌握的重要内容和关键问题。在阅读的过程中,您将会经历创建一个非常基础的游戏骨架,然后可以对其进行自定义,使其适用于任何类型的游戏。
项目创建
在开始新建一个游戏时首先要完成的事情是,创建各种可以用来保存您的游戏脚本和内容的项目目录。需要注意的是虚幻引擎 3 的设计是每次只能创建一个单独的项目。如果您需要创建多个项目,那么最好使用多个安装,因为在一个单独的安装中转换多个项目非常麻烦令人费解。
UnrealScript 项目
任何新游戏项目最终都会需要使用 UnrealScript 创建自定义类来构成游戏的游戏性。添加新 UnrealScripts 需要创建一个自定义 UnrealScript 项目,以便保存脚本,这些脚本已经被编译为一个新的 UnrealScript 软件包(.u 文件)。
要创建一个 UnrealScript 项目,首先要在您的虚幻安装中找到 ..DevelopmentSrc
目录。在目录内部,使用您希望命名给项目的名称创建一个新文件夹。通常,它是您的游戏名称的缩写版本,后面紧接着 "Game"。例如,Unreal Tournament 游戏使用的是 UTGame
。
在这里创建的示例游戏中,UnrealScript 项目将会被命名为 UDNGame
。创建完成后,新文件夹位于 ..DevelopementSrc
目录中:
在这个 UDNGame
文件夹中,创建一个新的文件夹,名为 Classes
。这是真正要包含项目脚本的文件夹。
要了解设置自定义 UnrealScript 项目的详细指南,请参阅自定义 UnrealScript 项目页面。
内容目录
没有内容,游戏就不能称之为游戏。您必须在屏幕上显示视觉元素,播放音频等等,当然还需要使用地图将所有内容组合在一起,然后将这些内容显示给玩家。通常,内容存储在软件包中(地图也是一个软件包),软件包本身存储在虚幻安装的 ..[GameName]Content
目录中。通常在可以组织软件包和地图的 Content
目录中提供了诸如 Characters
、 Environments
、 Maps
和 Sounds
这样的文件夹等等。
如果使用的是 UDK,示例内容中都包含这些子文件夹。您可以轻而易举地将您的游戏自定义内容软件包和地图保存在这些文件夹中,一切都会正常运作。然而,如果您希望将您的自定义内容与其他内容区分开,那么您只需在 Content
目录中创建一个新的文件夹,可以用您的游戏名称命名,然后将您的软件包保存在这个文件夹中。您甚至可以在这个文件夹中添加多个文件夹,使用与 Content
目录中相似的方式组织软件包。
游戏可玩性类
我们的示例游戏框架几乎全部是由一些在 UnrealScript 中创建并保存在我们的自定义 UnrealScript 项目中的重要游戏可玩性类组成。使用这些类的目的是制作一个可行但是通用或普通的游戏性体验。我们目前创建的不是最终游戏。现在的目的是创建一个项目,您可以将其作为创建您的游戏的基础。
相机
所有游戏的一个最基本内容是玩家观看这个世界的方式。在虚幻中,玩家视点的位置和方位由 PlayerController
类的 GetPlayerViewPoint()
函数控制。默认情况下,如果提供了玩家相机对象,可以使用它处理计算结果。它可以使用下面两个方法中的其中一种计算位置和方位:
- 通过
UpdateCamera()
函数调用Pawn
中的CalcCamera()
。 - 直接在
UpdateCamera()
函数中计算视角。
基础示例游戏将会使用第三人称视角。如上所述,可以通过使用 CalcCamera()
函数在 Pawn
类中进行这项操作,但是将相机逻辑规则封装在一个单独类中的设计理念以及使用一个提供所有相机功能(例如后期处理和相机动画)访问权限的 camera 类的实际情况,这个示例将会使用一个自定义 Camera
类控制定位相机。尽管该示例使用的是第三人称视角,但是通过更改计算相机位置和旋转的逻辑规则可以轻松地实现任何其他相机类型。
相机技术指南中提供了相机的详细说明以及其他视角实现的示例。
class UDNPlayerCamera extends Camera; var Vector CamOffset; var float CameraZOffset; var float CameraScale, CurrentCameraScale; /** 默认相机距离的乘数 */ var float CameraScaleMin, CameraScaleMax; function UpdateViewTarget(out TViewTarget OutVT, float DeltaTime) { local vector HitLocation, HitNormal; local CameraActor CamActor; local Pawn TPawn; local vector CamStart, CamDirX, CamDirY, CamDirZ, CurrentCamOffset; local float DesiredCameraZOffset; // 不要在插值的过程中更新输出观察目标 if( PendingViewTarget.Target != None && OutVT == ViewTarget && BlendParams.bLockOutgoing ) { return; } // 视图目标上的默认 FOV OutVT.POV.FOV = DefaultFOV; // 浏览相机 actor。 CamActor = CameraActor(OutVT.Target); if( CamActor != None ) { CamActor.GetCameraView(DeltaTime, OutVT.POV); // 通过 CameraActor 获取长宽比。 bConstrainAspectRatio = bConstrainAspectRatio || CamActor.bConstrainAspectRatio; OutVT.AspectRatio = CamActor.AspectRatio; // 查看 CameraActor 是否需要覆盖使用的 PostProcess 设置。 CamOverridePostProcessAlpha = CamActor.CamOverridePostProcessAlpha; CamPostProcessSettings = CamActor.CamOverridePostProcess; } else { TPawn = Pawn(OutVT.Target); // 为 Pawn Viewtarget 提供了一个指定相机位置的机会。 // 如果 Pawn 没有覆盖相机视图,那么我们将会继续使用我们自己的默认设置 if( TPawn == None || !TPawn.CalcCamera(DeltaTime, OutVT.POV.Location, OutVT.POV.Rotation, OutVT.POV.FOV) ) { /************************************** * Calculate third-person perspective * Borrowed from UTPawn implementation **************************************/ OutVT.POV.Rotation = PCOwner.Rotation; CamStart = TPawn.Location; CurrentCamOffset = CamOffset; DesiredCameraZOffset = 1.2 * TPawn.GetCollisionHeight() + TPawn.Mesh.Translation.Z; CameraZOffset = (DeltaTime < 0.2) ? DesiredCameraZOffset * 5 * DeltaTime + (1 - 5*DeltaTime) * CameraZOffset : DesiredCameraZOffset; CamStart.Z += CameraZOffset; GetAxes(OutVT.POV.Rotation, CamDirX, CamDirY, CamDirZ); CamDirX *= CurrentCameraScale; TPawn.FindSpot(Tpawn.GetCollisionExtent(),CamStart); if (CurrentCameraScale < CameraScale) { CurrentCameraScale = FMin(CameraScale, CurrentCameraScale + 5 * FMax(CameraScale - CurrentCameraScale, 0.3)*DeltaTime); } else if (CurrentCameraScale > CameraScale) { CurrentCameraScale = FMax(CameraScale, CurrentCameraScale - 5 * FMax(CameraScale - CurrentCameraScale, 0.3)*DeltaTime); } if (CamDirX.Z > TPawn.GetCollisionHeight()) { CamDirX *= square(cos(OutVT.POV.Rotation.Pitch * 0.0000958738)); // 0.0000958738 = 2*PI/65536 } OutVT.POV.Location = CamStart - CamDirX*CurrentCamOffset.X + CurrentCamOffset.Y*CamDirY + CurrentCamOffset.Z*CamDirZ; if (Trace(HitLocation, HitNormal, OutVT.POV.Location, CamStart, false, vect(12,12,12)) != None) { OutVT.POV.Location = HitLocation; } } } // 最后应用相机修改器(例如,视图浮动) ApplyCameraModifiers(DeltaTime, OutVT.POV); } defaultproperties { CamOffset=(X=12.0,Y=0.0,Z=-13.0) CurrentCameraScale=1.0 CameraScale=9.0 CameraScaleMin=3.0 CameraScaleMax=40.0 }
PlayerController
所有游戏的另一个基础内容是如何处理玩家的输入信息以及如何对其进行转换控制游戏,直接控制画面上的主要角色,还是使用点击界面控制游戏,或者是任何其他控制游戏的方法。显而易见,负责确定玩家控制游戏的方式的类是 PlayerController
类。
基础 PlayerController
执行函数足以使玩家可以到处跑,因为它们可以处理玩家输入信息并将其转化为运动。在这个示例中使用的自定义 PlayerController
类只是用来对我们在上面创建的自定义 Camera
类进行赋值。当然,在您不断丰富您的游戏内容过程中,您一定会需要修改这个类,以便添加您的游戏特定执行函数需要的逻辑规则。
角色技术指南详细说明了 PlayerController
类以及如何定制它使其适用于新的游戏类型。
class UDNPlayerController extends GamePlayerController; defaultproperties { CameraClass=class'UDNGame.UDNPlayerCamera' }
Pawn
虽然 PlayerController
类可以确定如何使用玩家输入信息控制游戏,在这个示例中将会涉及直接控制角色,角色的视觉表现形式和决定他与物理世界之间的交互的逻辑规则都封装在 Pawn
类中。在这个示例中的自定义Pawn
类将不会为与环境之间的交互添加任何特殊逻辑规则,但是它将会负责设置角色的视觉表现形式。也就是说它需要设置一个骨架网格物体、动画和物理资源来显示游戏中的角色。
角色技术指南详细说明了 Pawn
类以及它与 PlayerController
类合作的工作原理。
class UDNPawn extends Pawn; var DynamicLightEnvironmentComponent LightEnvironment; defaultproperties { WalkingPct=+0.4 CrouchedPct=+0.4 BaseEyeHeight=38.0 EyeHeight=38.0 GroundSpeed=440.0 AirSpeed=440.0 WaterSpeed=220.0 AccelRate=2048.0 JumpZ=322.0 CrouchHeight=29.0 CrouchRadius=21.0 WalkableFloorZ=0.78 Components.Remove(Sprite) Begin Object class="DynamicLightEnvironmentComponent" Name=MyLightEnvironment bSynthesizeSHLight=TRUE bIsCharacterLightEnvironment=TRUE bUseBooleanEnvironmentShadowing=FALSE End Object Components.Add(MyLightEnvironment) LightEnvironment=MyLightEnvironment Begin Object class="SkeletalMeshComponent" Name=WPawnSkeletalMeshComponent //您的网格物体属性 SkeletalMesh=SkeletalMesh'CH_LIAM_Cathode.Mesh.SK_CH_LIAM_Cathode' AnimTreeTemplate=AnimTree'CH_AnimHuman_Tree.AT_CH_Human' PhysicsAsset=PhysicsAsset'CH_AnimCorrupt.Mesh.SK_CH_Corrupt_Male_Physics' AnimSets(0)=AnimSet'CH_AnimHuman.Anims.K_AnimHuman_BaseMale' Translation=(Z=8.0) Scale=1.075 //通用网格物体属性 bCacheAnimSequenceNodes=FALSE AlwaysLoadOnClient=true AlwaysLoadOnServer=true bOwnerNoSee=false CastShadow=true BlockRigidBody=TRUE bUpdateSkelWhenNotRendered=false bIgnoreControllersWhenNotRendered=TRUE bUpdateKinematicBonesFromAnimation=true bCastDynamicShadow=true RBChannel=RBCC_Untitled3 RBCollideWithChannels=(Untitled3=true) LightEnvironment=MyLightEnvironment bOverrideAttachmentOwnerVisibility=true bAcceptsDynamicDecals=FALSE bHasPhysicsAssetInstance=true TickGroup=TG_PreAsyncWork MinDistFactorForKinematicUpdate=0.2 bChartDistanceFactor=true RBDominanceGroup=20 bUseOnePassLightingOnTranslucency=TRUE bPerBoneMotionBlur=true End Object Mesh=WPawnSkeletalMeshComponent Components.Add(WPawnSkeletalMeshComponent) Begin Object Name=CollisionCylinder CollisionRadius=+0021.000000 CollisionHeight=+0044.000000 End Object CylinderComponent=CollisionCylinder }
HUD
HUD
类主要负责向玩家显示游戏的相关信息。信息以及显示方式完全由游戏指定。因为这样,这个示例执行函数将只需提供一个空白的石板,您可以通过它创建您自己的自定义 HUD,可以使用 Canvas
对象或Scaleform GFx。
HUD 技术指南提供了关于在虚幻引擎 3 中使用 Canvas
对象或 Scaleform GFx 集成创建抬头显示器的详细信息。
class UDNHUD extends MobileHUD; defaultproperties { }
Gametype
游戏类型是游戏的核心内容。它可以决定游戏的规则和游戏进行或结束的条件。说清楚一点就是它完全取决于游戏。游戏类型还可以负责通知引擎哪些类可以用作 PlayerControllers
、 Pawns
和 HUD
等等。游戏类型的示例执行函数只会在 defaultproperties
中指定这些类,然后剩下的执行函数由您决定。
游戏类型技术指南中进一步阐述了游戏类型的概念,而且会深入探讨定制您的自定义游戏类型这部分内容。
class UDNGame extends FrameworkGame; defaultproperties { PlayerControllerClass=class'UDNGame.UDNPlayerController' DefaultPawnClass=class'UDNGame.UDNPawn' HUDType=class'UDNGame.UDNHUD' bDelayedStart=false }
编译
要点: 您必须关闭编辑器才能编译脚本。如果编辑器正处于打开状态,那么请马上将其关闭。
为了将 UnrealScript 项目编译为一个可以使游戏性框架在游戏中使用的软件包,引擎需要多加注意这个项目。以上操作可以通过将项目添加到 DefaultEngine.ini
文件的 [UnrealEd.EditorEngine]
部分中的 EditPackages
数组完成。添加 UDNGame
项目的语法如下所示:
+EditPackages=UDNGame
如果您使用的是 UDK,那么您将会注意到在这部分还包含一些其他项目,即 UTGame
和 UTGameContent
项目。是否要这样做要根据您制作的游戏类型决定。现在,最终的 [UnrealEd.EditorEngine]
部分应该如下所示:
[UnrealEd.EditorEngine] +EditPackages=UTGame +EditPackages=UTGameContent +EditPackages=UDNGame
这里不乏强行编译脚本的方法。您可以使用下面的方法:
- 运行
Make
命令行开关 - 编译 UnrealFrontend
- 运行游戏或编辑器
上面任意一种方法都可以编译脚本。但是,我们将只需运行编辑器就可以自动编译脚本。运行编辑器,然后您应该会看到下面的对话框:
选择“是”,然后会出现一个控制台窗口,其中显示的是编译过程状态。
您应该会看到
Success - 0 error(s), 0 warning(s),前提是编译成功。如果出现错误,那么错误消息会通知您发生错误的文件名和行编号,这样可以使查找和解决错误的过程变得简单。
发生错误的文件名应该会在上面的图片中标明。行编号会显示在文件名后面的括号 () 中。同时还会显示错误的相关描述。这个错误将通知我们等号 (=) 后面的项无效。这可能是由于名称拼写错误、变量未声明等等原因造成的。解决这个错误,然后使用与上面相同的程序再次进行编译,最后编译才会成功。
测试
为了测试在这个示例中创建的游戏框架,您可以在下面两种方法中任选一种:
- 在虚幻编辑器中,在您的测试地图
WorldInfo
属性中将UDNGame
设置为 PIE 游戏类型。 - 在
DefaultGame.ini
配置文件中,将UDNGame
设置为引擎的默认游戏类型。
地图游戏类型
要将 UDNGame
游戏类型设置为供在虚幻编辑器或引擎中游戏时特殊地图使用的 Gametype,请从 View(视图) 菜单中选择 World Properties(世界属性) 。接下来将会显示 WorldInfo
属性窗口。
展开 Game Type 类别,然后找到 Gametype for PIE
属性。在可用的游戏类型列表中选择 UDNGame
。
现在,您可以在编辑器中使用 Play in PIE 或 Play from here 功能运行地图,与此同时它应该使用自定义游戏类型,通过完全可以控制的角色显示第三人称视角。
您还可以将 Default Game Type(默认游戏类型) 属性设置为 UDNGame
强制这个地图使用自定义游戏类型,无论 .ini 文件(下面将会进行讲解)中的默认设置是什么。
默认游戏类型
注意: 在编辑 .ini 文件时应该关闭编辑器。
要将 UDNGame
游戏类型设置为引擎的默认游戏类型,请在您的虚幻安装的 ..[GameName]Config
目录中打开 Defaultgame.ini
配置文件。这样在地图 URL
中没有指定游戏类型或地图没有游戏类型前缀的情况下,在所有的地图中都会使用这个游戏类型。在 [Engine.GameInfo]
项中,要将 UDNGame
游戏类型赋给三个属性。同时还要将 PlayerController
类赋给属性。最后,这里有一些额外的代码行,它们可以设置那些只适用于将会被删除的 UT3 示例游戏的游戏类型前缀。
最后的 [Engine.GameInfo]
项如下所示:
[Engine.GameInfo] DefaultGame=UDNGame.UDNGame DefaultServerGame=UDNGame.UDNGame PlayerControllerClassName=UDNGame.UDNPlayerController GameDifficulty=+1.0 MaxPlayers=32 DefaultGameType="UDNGame.UDNGame"
现在,在引擎中运行或者在虚幻编辑器中使用 Play on PC(在计算机上游戏) 功能的所有地图使用的应该都是自定义游戏类型 - 通过完全可以控制的角色显示第三人称视角(除非被 World(世界)属性中被地图 URL 或 Default Game Type(默认游戏类型) 覆盖。)