最近学习虚幻4,先跟着教程学习做个《吃豆人》小游戏,熟悉下引擎里的一些基本概念。
下面回顾一下学习过程。
引擎版本:4.22
visual studio 2019
一、新建项目
启动引擎后,新建C++基础项目,包含初学者资源,最后点击创建项目就可以了。
二、新建关卡和地形
选择 文件->新建关卡 或者 Ctrl+N 呼出新建关卡对话框,选择Default,这样就创建好一个默认关卡了。
首先先将默认地板删掉,然后在左侧的模式面板-放置页签-Geometry分类下选择Box,拖拽到场景中,然后在右侧的细节面板下找到Brush Settings,将x改为2700,y改为2100,z改为100(为什么?因为喜欢)。这样就创建好一个地板了。
然后找一张吃豆人的截图,或者自由发挥,用Box画刷再造一些墙体,大概就这样
最后别忘了保存关卡。
三、美化地形
这个黑白花的地形看起来过于迷惑,赶紧给加上材质。
在右侧的世界大纲视图中,找一个地形的画刷,右键->选择->选择所有表面,然后从下面Content Browser中的StarterContent->Materials下挑选一个材质(这个StarterContent就是创建项目时选择的初学者内容),将材质拖拽给右侧细节面板的“表面材质”。这样就不是黑白花了。
但是我想让墙和地板的样子看上去不同,怎么搞?这就简单了,直接在场景中单击地板,地板的细节面板已经有了“表面材质”一项,把ContentBrowser下的任意材质拖上去就可以了。
四、添加道具
这个游戏唯一的道具就只有豆子了吧。
文件->新建C++类 在弹出的对话框中选择父类为“Actor”,然后起个类名“Bean”,设置为公有,创建完成。
因为选择了新建“公有”类,所以头文件会被放到Public文件夹下,因此需要修改引入的头文件路径,不然会找不到头文件。在前面加上 Public/ 即可。
在头文件中有个 #include “XXX.generated.h” ,由于框架的限制,这句一定要放到最后引用
而在cpp文件中,对当前类的头文件的引用,却要放在最前面
这里有两个自动生成的生命周期方法,BeginPlay 和 Tick。要理解一个新框架,了解他的生命周期方法是很重要的,之后会找个时间专门学习生命周期方法,现在只看这两个。
BeginPlay 是当对象被添加到场景中时调用的,如果想获取一些在运行时才有的信息,并且要根据获取的信息在自己出场前进行调整,写在这个方法中就是最好的选择。
Tick 这是对象的自更新方法,需要使用 PrimaryActorTick.bCanEverTick = true; 来开启,开启后每一帧都会调用此方法。
现在这个豆子放到场景中还只是一个虚无的灵魂,接下来要给它加上表象和实体。
在头文件中引入"Components/SphereComponent.h"和"Components/StaticMeshComponent.h",记住一定要放在 generated.h 前面。然后声明两个变量
USphereComponent* BeanShape;
UStaticMeshComponent* BeanMesh;
UStaticMeshComponent继承自 UMeshComponent ,用来表示豆子的外貌,用户可以看见的样子。
USphereComponent 继承自 UShapeComponent ,用来表示豆子的实体形状,是不可见的,但其实这才是本体,参与碰撞检测。
接下来在构造方法中实例化他们。实例化有两种方式 UObject::CreateDefaultSubobject 和 NewObject 。如果想在c++编译时生成,在ue编辑器中可见的话,就要使用UObject::CreateDefaultSubobject,如果在游戏运行后才需要生成,使用NewObject,这里我们用CreateDefaultSubobject。
这里用球形表示豆子了,为了让豆子的表象和本体一致,需要让网格也像个球。
先引入"UObject/ConstructorHelpers.h",记得要在"Public/Bean.h"之后引入。
然后使用ConstructorHelpers::FObjectFinder<UStaticMesh> 创建一个网格,创建时需要传入网格资源的路径,首先切换到虚幻4编辑器,在新手资源中有个Shapes文件夹,在里面找到球,然后右键->复制引用
将复制好的路径字符串传给网格对象的构造方法,就可以了。最后还要判断网格获取是否成功,因为可能会有资源损坏或者丢失的情况,为了避免空指针错误,增强程序的健壮性,这是很有必要的。如果成功获取,就将获取的网格设置给BeanMesh。
切换到虚幻4编辑器,点击编译,编译完成后在ContentBrowser下C++类中找到刚才新建的Bean,拖拽到场景中。这样就在场景中添加好一个由C++创建的对象了。
但是通过观察我们发现,这个对象的Mesh和Sphere的大小和位置是不匹配的,因此还需要进行调整
这个调起来可就麻烦了,需要在代码中修改位置,再重新编译,再重新加到场景中看效果,效果不行再重复修改代码。。。当然不会是这样,我们首先在BeanMesh和BeanShape声明的时候,加上UPROPERTY,让这两个属性再UE4编辑器中可见。
然后保存编译,在ContentBrowser中找到Bean的C++类,右键->创建基于Bean的蓝图类,起个名字BP_Bean,确定进入蓝图编辑界面。
球网格的默认大小是直径100,我的地形创建的路宽也是100,有点大,缩小一些。先选中白色球体,在细节面板下找到Transform->Scale 都改成0.3,这样就是直径30的球了。
再选中红线构成的Shape,将半径改成15,这样就一样大了。
再选中白色球体,将z坐标调成 -15,终于灵魂与肉体合而为一,完全重合了。
最后别忘了编译和保存蓝图
将创建好的蓝图拖拽到场景中,调整下位置就好了。只是有点丑。
所以我们来给他点颜色,新建个文件夹,这里起名Materials,在里面右键->新建材质
起名M_Bean,双击打开,弹出材质面板。
在空白处按住V键点鼠标左键,注意是左键,弹出一个小面板,将白色点连接到Base Color,再双击黑色区域弹出颜色板,调出豆子的颜色
保存后在场景中选中豆子,将刚刚编辑好的材质拖拽到豆子的 细节面板->Materials->BeanMesh 上,豆子就变色啦。不过,再拖一个豆子蓝图进来,还是白色的,想要拖进来的也是黄色,就需要更改蓝图,将蓝图的BeanMesh设置为M_Bean
这个豆子傻傻的一动不动,这样是没有灵魂的,他不是有个自更新函数吗,我们可以让他动起来的,这个比较简单,用sin函数和DeltaTime的累计值去计算一个偏移量,加在原始位置上就可以平滑的上下漂浮了。但是考虑到满屏幕几百个豆子,动起来都一个节奏,也挺怪异的,所以我又用随机数给他一个初始的Timer值。
编译后在场景中再拖入几个豆子蓝图,点击播放
可以,下面可以用豆子铺满整个场景了。因为这里豆子的初始Z值都是相同的,建议使用顶端视图来进行操作
这个体力活做完大概是这样
五、创建玩家
新建一个C++文件,继承Character,起名PacmanPlayer。
相比豆子类,多了一个方法 SetupPlayerInputComponent 。这是干什么用的呢?这也是个由框架调用的方法,会传入一个UInputComponent参数,我们可以通过这个参数来绑定用户的输入事件和Character的响应函数。
如何绑定用户输入事件?首先,切换到UE4,打开 编辑->项目设置 ,然后找到 引擎->输入 ,点击输入后在右侧找到Bindings,可以看到两个映射
其中 Action Mappings 表示点击或抬起时会触发的事件,Axis Mappings 表示按住就会连续触发的事件,这里想按住方向键让玩家一直走,所以选择第二种。
新建两个移动事件 moveX 和 moveY 。通过观察场景中的Player Start可以知道,X轴的正方向是前,Y轴的正方向是右。因此,用A、D键表示左右移动,W、S键表示上下移动。
回到Visual studio,在PacmanPlayer中声明一个 FVector 对象,用来表示移动的方向。声明两个函数,用来绑定moveX和moveY事件。
具体实现如下
虚幻4有个强大功能,除了用C++编写游戏,还能用蓝图编写游戏。之前的豆子用C++设置了外形,这次我们使用蓝图来创建玩家的形象。
先创建基于PacmanPlayer的蓝图,在左上角的Components标签下找到AddComponent,添加一个Sphere(没错,玩家也是个球。。)然后设置球的Scale为0.8,玩家大概就是一个直径80的球了。这里有个默认的碰撞体,是胶囊状的,把他的高和半径改成40,就神形合一了。当然,别忘了在蓝图的细节面板中给球换个材质
接下来打开C++类下的Pacman文件夹,找到一个PacmanGameModeBase文件。这是用来定义游戏模式的,可以在里面进行涉及游戏性的各种设定。我们找他是为了设置玩家为刚刚创建的BP_PacmanPlayer。
蓝图真香,完全不想写代码了。创建个基于PacmanGameModeBase的蓝图,在里面找到细节面板的Classed下的Default Pawn Class ,把值改为PacmanPlayer的蓝图类
编译保存后,再打开 项目设置 ,找到 项目->地图&模式->Default Modes 将Default Game Mode改为刚刚创建的游戏模式蓝图。
*也可在在这里更改Default Maps为新创建的关卡,这样下次打开项目时,就不是那两把椅子的关卡啦。
这时在播放游戏,就可以用刚刚设置的按键来控制主角移动了。但是这是第一人称视角,而吃豆游戏是上帝视角的。
所以我们来创建一个上帝视角的摄像机。
首先在左侧模式面板下搜索Camera,然后将摄像机拖到场景中,就会在场景右下放展示出摄像机的视角,通过对摄像机的旋转和平移,调整好视角。
在场景面板上面的按钮中,找到蓝图Blueprints,在里面选择打开关卡蓝图
我们虽然生成了上帝视角摄像机,但是并没有设置给游戏,打开关卡蓝图就是要设置摄像机的。
是不是又看到了熟悉的 BeginPlay 和 Tick ?没错,蓝图就是代码的另一种表象形式。
确保你的摄像机是选中状态,然后在关卡蓝图中右键就会出现“创建一个到Camera Actor的引用”选项,点击就创建了这个摄像机的引用。
我们要把摄像机给玩家,所以还要获取玩家控制器的引用,右键,搜素Get Player Controller。
我们拿到摄像机和控制器之后,还要拿到一个把摄像机设置给控制器的函数,右键搜素 Set View Target With Blend。
接下来我们要开始做连线题了。
设置摄像机这个事情,在什么时候触发呢?在进入场景时,因此吗BeginPlay的右箭头连到Set View Target With Blend的左箭头。
设置方法是由谁来调用呢?由player Controller调用,因此将Get Player Controller的返回值连接到Set View Target With Blend的Target。
最后要设置谁为新的ViewTarget呢?就是我们新创建的摄像机了。将Camera Actor连接到New View Target。
编译保存
这是在播放游戏,就是上帝视角了。
然后发现豆子是不能吃的。那我们接下来就做吃豆子.
切换到Visual Studio,打开PacmanPlayer,因为Character默认的碰撞体是个胶囊体,那我们就在BeginPlay中获取胶囊体组件,给他设置一个碰撞监听。
碰撞监听有两种,Hit 和 Overlap。Hit 适用于物理碰撞,例如走路撞到石头。Overlap 是区域重叠,例如走进一个魔法阵。我们这里不希望豆子被玩家撞飞,也不需要影响玩家行动,所以使用了Overlap。
可能需要先include胶囊体组件。然后用GetCapsuleComponent获取胶囊体,用里面的OnComponentOverlap对象.AddDynamic来添加监听器,但是监听器的定义是什么样的呢?我们Ctrl点进OnComponentOverlap,发现他是个FComponentBeginOverlapSignature类型的对象点,进这个类又看到一个宏DECLARE_DYNAMIC_MULTICAST_DELEGATE_SixParams,再点进这个宏......对不起打扰了。回到上一页,可以看出这个监听器是需要6个参数的,后面已经列出了参数类型,抄一份走。
在PacmanPlayer中声明一个碰撞监听函数,叫OnCollision,形参就是刚复制来的6个参数,很重要的一点是,要在这个函数上一行写上UFUNCTION(),不然运行时会提示错误。然后在cpp中实现他,并将这个函数绑给胶囊体组件。
接下来实现碰撞监听。先判断是谁撞过来了,如果是豆子,就吃掉他。需要先include豆子类。
修改碰撞预设值。
打开项目设置->引擎->Collision ,新建两个对象通道,起名 Bean 和 Pacman 。然后展开Preset列表,新建两个描述文件,同样起名Bean和Pacman,对象类型选择刚刚新建的两个对象通道。注意把Bean描述文件的碰撞响应中的Pacman勾选为Overlap,同样Pacman中的Bean也勾选为Overlap。
然后分别打开Pacman和Bean的蓝图,选中碰撞体(这里Pacman是Capsule,Bean是Sphere),在细节面板中找到Collision,将碰撞预设值改为刚刚新建的描述文件Pacman和Bean
点击播放,就可以吃豆了
六、添加敌人
在原游戏中,中间的空地是给敌人的出生点,所以这里先把玩家移到他该在的位置。选择一个适合的位置,把Player Start移过去。
这个地方看着不错,就在这挖个空地当玩家出生点吧。选择Box画刷,勾选下面的挖空型,然后去墙上打洞就可以了。
为敌人新建几不同颜色的材质M_Enemy1~4,新建一个敌人碰撞通道Enemy,设置敌人与敌人,敌人与豆子之间的碰撞响应为忽略。
新建敌人类Enemy继承Character,在头文件中声明一个UstaticMeshComponent对象来定义敌人的形象,加上UPROPERTY(EditAnywhere)以便在UE4中调整他。因为敌人在玩家吃到超级豆子后,会有冰冻减速的效果,所以再声明两个UMaterialInterface对象,用来更换敌人的材质。这次我们把敌人设置为圆柱体的形象。
新建敌人的蓝图,调整好Mesh和Shape的大小,设置好碰撞通道,就可以把敌人拖进场景了,然后给他们加上不同的材质。
再回到C++,在构造方法中用ConstructHelpers找到冰冻材质赋值给EnemyMaterialIce
在BeginPlay中用EnemyMesh->GetMaterial(0) 获取当前的材质,赋值给EnemyMaterialNormal
超级豆、敌人攻击、被攻击的事情先放一放,我们先让这个敌人走动起来。
要让敌人自动寻路,首先添加个导航网格。在 模式->体积 下找到Nav Mesh Bounds Volume,拖拽到场景中。调整尺寸,跟地图一样大(x:2700,y:2100,z:20),按P键显示导航。绿色的就是可到达的范围,一开始可能不会太顺利,我反复调整了导航网格的高度和位置,还在项目设置->引擎->导航网格物体->Generation 下调整了Cell Size 和Cell Height,最终才得到想要的效果。
可以先编辑敌人的蓝图,对网格做个简单的测试。如果敌人不能移动,可以将玩家,敌人,地板,网格的Z坐标升高,避开一切障碍物再测试。动了就说明逻辑没问题,需要继续调整参数。。。
测试没问题后删掉新增的4个逻辑块,我们用C++来实现敌人的移动。
新建C++类EnemyAIController,继承AIController,选择父类时找不到这个类可以点击右上角的“显示所有类”然后搜素AIController
然后打开项目的Pacman.Build.cs 文件,在PublicDependencyModuleNames 的Range里追加一个 “NavigationSystem” 。在4.22版本是需要手动引入导航系统模块的。
接下来实现EnemyAIContorller,声明一个SearchNextPosition函数,用来查找并移动到下一个目标点,重写OnMoveCompleted方法,这是当移动完成时的回调,我们在这里继续调用SearchNextPosition。
最后实现SearchNextPosition。
接下来修改Enemy类
首先在构造方法中,设置将控制权自动交给AI控制器 AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned;
然后用反射的方式绑定AI控制器 AIControllerClass = AEnemyNewController::StaticClass();
最后,在BeginPlay中获取控制器,并开始移动。
保存、编译、播放,敌人已经开始四处乱窜了。注意敌人与敌人,敌人与豆子之间的碰撞响应要设为忽略,不然他们会撞到一起的。
七、走向胜利
没想到已经写了这么多,这篇日记就先到这里吧,下篇接着写。