unreal 创建蓝图_unreal 蓝图 - CSDN
  • 前面两篇博客我们都是通过ExpandNode来实现蓝图节点的功能,这一篇来介绍另外一种重要的蓝图扩展方式,就是Node Handler。

    通过前面的文章,我们已经能够创建自己的蓝图节点,并可以动态添加、删除Pins,但是感觉好像有什么地方不太对劲啊。你发现没有?那就是前面两篇文章中,我们自定义的蓝图节点都是通过UK2Node::ExpandNode()来实现节点的具体功能,然而,这个函数只不过是在内部创建了一些其他的节点,然后把自己的Pins重新连接到新建的节点的Pin之上,本质上这个过程手动连线也可以做啊!如果,我们需要做一个全新的蓝图功能节点,无法用现有节点组合完成呢?那要怎么办呢?那就需要深入到蓝图的编译过程,控制蓝图编译出的字节码,来实现想要的节点功能了。引擎中实现的大多数默认节点都是这样做的。在这篇博客,就通过一个最简单的实例,来探索这个过程是怎么实现的。

    在进入实做的细节之前,我们必须先谈一点概念性的抽象的东西,概念搞明白了之后,我们再通过一个实例来看一下具体的实现步骤。

    浅谈蓝图编译过程

    由于本人对蓝图编译的过程掌握的还不够,还不能非常详实的把它的原理和实现都说的很明白,所以这里只能“浅谈”一下,谈个大概。在以后的博文中再进行补充吧。

    • 蓝图编译过程的最终产出是一个:UBlueprintGeneratedClass对象。UBlueprintGeneratedClass它是从UClass派生的,也就是说它具备Unreal C++开发的类所具备的那些UProperty啊、UFunction啊等等东西;

    • 蓝图里面使用可视化Graph编辑的那些逻辑,最终会生成字节码,保存到UFunction成员变量中,具体就是:TArray<uint8>``UFunction::Script 这个成员变量啦;

    • 字节码生成的核心过程是

      1. 遍历Graph的所有节点,使用一定策略(具体是啥策略,另外的文章再讲)生成一个线性的列表,保存到“TArray<UEdGraphNode*>``FKismetFunctionContext::LinearExecutionList”;
      2. 然后遍历每个蓝图节点,生成相应的“语句”,正确的名词是:Statement,保存到“TMap< UEdGraphNode*, TArray<FBlueprintCompiledStatement*> > FKismetFunctionContext::StatementsPerNode”,一个Node在编译过程中可以产生多个Statement;
        重点来了:这就是我们开发的自定义节点能够控制字节码生成的地方。
      3. Statement 有很多类型,看看它的枚举,发现很接近字节码了,是类似汇编语言那种;需要通过“条件跳转”之类的逻辑,在线性的代码中产生分支和循环;详见下图中的:enum EKismetCompiledStatementType
      • 上述过程可以算是编译器的前端,接下来就进入后端的流程,具体代码是在:class ``FKismetCompilerVMBackend
      • 后端,也就是字节码的生成的核心代码是在:FScriptBuilderBase::GenerateCodeForStatement(),这个函数通过一个大的“switch (Statement.Type)”语句,把不同类型的statement生成字节码
        在这里插入图片描述
        那么,在前面提到的“重点步骤”是怎么实现呢?很简单,分两步:
    • 定义一个class FNodeHandlingFunctor的派生类,重载其方法,例如:Compile()等,即可控制这个节点在编译过程中生成的statement;

    • 重载 class UK2Node的虚函数“CreateNodeHandler()”,返回一个上述派生类的对象指针。

    FNodeHandlingFunctor 详解

    既然本文重点步骤就是编写FNodeHandlingFunctor的派生类,那么有必要把这个基类仔细的看看啦!
    这个类的代码并不多,但是包含了三个重要的概念:

    • Statement:这个前面已经讲过了,它对应的是“struct FBlueprintCompiledStatement”。这个结构体有很多变量,但是并不是同时有效的,具体要根据Type字段来解释。其中的LHS和RHS是两个常用的字段,也就是我们常说的“左值”和“右值”。(简单说就是:一个表达式把一系列右侧的参数值计算之后赋值给左侧的变量)
    • Terminal,也就是:struct FBPTerminal,它的注释说的比较明白:A terminal in the graph (literal or variable reference),也就是说“它代表Graph中的一个端点,可能是字面量,也可能是变量的引用”
    • Net:对于“变量引用型的Terminal”,需要注册的一个“关系网”中,用来在运行时求解它的值。

    理解了这三个概念之后,再来看他的几个常用的虚函数:

    • virtual void Compile(FKismetFunctionContext& Context, UEdGraphNode* Node)
      这个就是编译过程中的回调啦,一般用来生成这个Node对应的Statement,可以是0个到多个;
    • virtual void RegisterNets(FKismetFunctionContext& Context, UEdGraphNode* Node)
      这个节点注册Terminal网络的回调;在这里可以使用“FKismetFunctionContext::CreateLocalTerminal”创建非Pin直接相关的Terminal对象;
    • virtual void RegisterNet(FKismetFunctionContext& Context, UEdGraphPin* Pin)
      这个节点上的针脚注册Terminal网络的回调;

    总结一下:

    • 实现一个FNodeHandlingFunctor的派生类,我们可以实现自己的Node Handler
    • 通过这个Node Handler,可以在编译过程中生成需要的Terminal,并注册到Net中
    • 在编译的过程中,可以生成任意多个Statement,直接影响后续的字节码生成

    举个栗子

    下面我们就通过一个具体的例子,来看看通过Node Handler方式控制蓝图节点的编译,具体如何实现的。说实话,引擎实现的蓝图节点真的很丰富了,很难想出一个有实用性的例子,只好胡诌一个了:

    • 判断输入的一个整型变量,分为:正数,零,负数,三种状态,执行不同的流程;

    如下图中的“TriGate”节点所示:
    在这里插入图片描述

    完整的Demo工程可以从我的GitHub下载:https://github.com/neil3d/UnrealCookBook

    这个节点的完整源代码附在文末,我们先来step by step讲解一下,实现过程不难理解:

    第一步:添加一个自定义的UK2Node派生类

    首先就是要创建一个class UK2Node的派生类:class UBPNode_TriGate : public UK2Node,这个过程很简单,基本上和前面两篇博客介绍的一样,这里就不重复了。只有一个地方不同,那就是我们不再需要重载 ExpendNode() 函数,而是重载CreateNodeHandler()函数。它的实现也很简单,就是返回一个我们自定义的FNodeHandlingFunctor派生类对象。

    FNodeHandlingFunctor * UBPNode_TriGate::CreateNodeHandler(FKismetCompilerContext & CompilerContext) const
    {
    	return new FKCHandler_TriGate(CompilerContext);
    }
    

    在其cpp文件中创建一个自定义的FNodeHandlingFunctor派生类:class FKCHandler_TriGate : public FNodeHandlingFunctor,后面将主要实现这个类的几个虚函数,来完成整个节点的功能。

    第二步:生成两个Terminal

    想象一下,代码执行过程中,我们需要比较输入的那个整数是否大于零,把结果保存到一个临时变量中,所以我们需要两个Terminal:

    • 一个用来用来表示字面量“0”
    • 另一个用来存储比较结果

    这两个Terminal就是在"FKCHandler_TriGate::RegisterNets()"函数中定义的

    	virtual void RegisterNets(FKismetFunctionContext& Context, UEdGraphNode* Node) override
    	{
    		FNodeHandlingFunctor::RegisterNets(Context, Node);
    
            // 存储比较结果的bool变量
    		FBPTerminal* BoolTerm = Context.CreateLocalTerminal();
    		BoolTerm->Type.PinCategory = UEdGraphSchema_K2::PC_Boolean;
    		BoolTerm->Source = Node;
    		BoolTerm->Name = Context.NetNameMap->MakeValidName(Node) + TEXT("_CmpResult");
    		BoolTermMap.Add(Node, BoolTerm);
    
            // 字面量“0”
    		LiteralZeroTerm = Context.CreateLocalTerminal(ETerminalSpecification::TS_Literal);
    		LiteralZeroTerm->bIsLiteral = true;
    		LiteralZeroTerm->Type.PinCategory = UEdGraphSchema_K2::PC_Int;
    		LiteralZeroTerm->Name = TEXT("0");
    	}
    

    第三步:实现Compile过程,生成6个Statement

    做好了前面两步的准备,接下来就是关键的步骤了:定义一系列Statements来实现我们的逻辑。重复一下要实现的逻辑:

    • 判断输入的一个整型变量,分为:正数,零,负数,三种状态,执行不同的流程;

    逻辑很简单,不过,我们需要转换一下思考方式,要使用类似汇编语言的那种思路:要把语句顺序排列,然后使用条件跳转语句控制分支逻辑。下面将要使用到的Statement类型先说明一下:

    • KCST_CallFunction:调用指定的UFunction,我们需要把“输入那个整数”和零做比较,这个功能我们将通过调用 class UKismetMathLibrary 中的函数来实现,使用到两个函数:
      1. UKismetMathLibrary::Greater_IntInt()
      2. UKismetMathLibrary::EqualEqual_IntInt()
    • KCST_GotoIfNot:条件跳转,可以指定跳转到哪个Statement(或者指定的Pin);
    • KCST_UnconditionalGoto:无条件跳转,主要用来跳转到右侧的三个Exec Pin中的一个;

    KCST_CallFunction 实例

    下面说一下KCST_CallFunction具体在我们这个例子中的使用。

    首先我们需要找到UFunction相关的信息:

    UClass* MathLibClass = UKismetMathLibrary::StaticClass();
    UFunction* CreaterFuncPtr = FindField<UFunction>(MathLibClass, "Greater_IntInt");
    UFunction* EqualFuncPtr = FindField<UFunction>(MathLibClass, "EqualEqual_IntInt");
    

    下面生成的这个Statement就是比较输入变量是否大于零,并把比较结果保存到我们定义的BoolTerm变量中:

        FBlueprintCompiledStatement& CallCreaterZero = Context.AppendStatementForNode(MyNode);
    		CallCreaterZero.Type = KCST_CallFunction;
    		CallCreaterZero.FunctionToCall = CreaterFuncPtr;
    		CallCreaterZero.LHS = BoolTerm;
    		CallCreaterZero.RHS.Add(InputTerm);
    		CallCreaterZero.RHS.Add(LiteralZeroTerm);
    

    KCST_GotoIfNot 实例

    下面生成的这个Statement就是通过判断BoolTerm的值为False的话,则跳转:

    		FBlueprintCompiledStatement& IfPositive = Context.AppendStatementForNode(Node);
    		IfPositive.Type = KCST_GotoIfNot;
    		IfPositive.LHS = BoolTerm;
    

    那么,跳转到什么地方呢?跳转到后面的比较语句,具体代码如下:

        FBlueprintCompiledStatement& CallEqualZero = Context.AppendStatementForNode(MyNode);
        ...
    		CallEqualZero.bIsJumpTarget = true;
    		IfPositive.TargetLabel = &CallEqualZero;
    

    设置新的语句“bIsJumpTarget = true”,使它成为一个跳转的目标,然后把前面那个跳转的Statement的TargetLabel设置为这个新的语句。

    KCST_UnconditionalGoto

    无条件跳转到指定的Exec Pin。注意:下面这个Statement在运行时,根据BoolTerm的值,可能被前面的条件跳转语句跳过,来实现分支的逻辑。

      FBlueprintCompiledStatement& ExecZero = Context.AppendStatementForNode(Node);
    	ExecZero.Type = KCST_UnconditionalGoto;
    	Context.GotoFixupRequestMap.Add(&ExecZero, ZeroPin);
    

    完整的 Statement 列表

    运用上面三种Statement,把他们罗列出来,即可实现我们的逻辑功能了。乍看上去可能并不直观,主要是这种类似汇编的方式并不直观,可能需要反复看几遍。

    	virtual void Compile(FKismetFunctionContext& Context, UEdGraphNode* Node) override
    	{
    		UBPNode_TriGate* MyNode = CastChecked<UBPNode_TriGate>(Node);
    
    		// 查找输入的那个整数的Pin对应的Terminal
    		UEdGraphPin* InputPin = Context.FindRequiredPinByName(Node, TriGatePN::Input, EGPD_Input);
    
    		UEdGraphPin* PinToTry = FEdGraphUtilities::GetNetFromPin(InputPin);
    		FBPTerminal** pInputTerm = Context.NetMap.Find(PinToTry);
    		if (pInputTerm == nullptr)
    		{
    			CompilerContext.MessageLog.Error(TEXT("FKCHandler_TriGate: Failed to resolve term passed into"), InputPin);
    			return;
    		}
    
    		FBPTerminal* InputTerm = *pInputTerm;
    
    		// 查找三个输出Pin
    		UEdGraphPin* PositivePin = MyNode->FindPin(TriGatePN::Positive, EGPD_Output);
    		UEdGraphPin* ZeroPin = MyNode->FindPin(TriGatePN::Zero, EGPD_Output);
    		UEdGraphPin* NegativePin = MyNode->FindPin(TriGatePN::Negative, EGPD_Output);
    
    		// 临时bool变量的Terminal
    		FBPTerminal* BoolTerm = BoolTermMap.FindRef(MyNode);
    
    		UClass* MathLibClass = UKismetMathLibrary::StaticClass();
    		UFunction* CreaterFuncPtr = FindField<UFunction>(MathLibClass, "Greater_IntInt");
    		UFunction* EqualFuncPtr = FindField<UFunction>(MathLibClass, "EqualEqual_IntInt");
    
    		// Statement 1: 计算表达式 BoolTerm = Interger > 0
    		FBlueprintCompiledStatement& CallCreaterZero = Context.AppendStatementForNode(MyNode);
    		CallCreaterZero.Type = KCST_CallFunction;
    		CallCreaterZero.FunctionToCall = CreaterFuncPtr;
    		CallCreaterZero.LHS = BoolTerm;
    		CallCreaterZero.RHS.Add(InputTerm);
    		CallCreaterZero.RHS.Add(LiteralZeroTerm);
    
    		// Statement 2: if(BoolTerm)
    		FBlueprintCompiledStatement& IfPositive = Context.AppendStatementForNode(Node);
    		IfPositive.Type = KCST_GotoIfNot;
    		IfPositive.LHS = BoolTerm;
    
    		// Statement 3: 执行 Positive Pin
    		FBlueprintCompiledStatement& ExecPositive = Context.AppendStatementForNode(Node);
    		ExecPositive.Type = KCST_UnconditionalGoto;
    		Context.GotoFixupRequestMap.Add(&ExecPositive, PositivePin);
    
    		// Statement 4: 计算表达式 BoolTerm = Interger == 0
    		FBlueprintCompiledStatement& CallEqualZero = Context.AppendStatementForNode(MyNode);
    		CallEqualZero.Type = KCST_CallFunction;
    		CallEqualZero.FunctionToCall = EqualFuncPtr;
    		CallEqualZero.LHS = BoolTerm;
    		CallEqualZero.bIsJumpTarget = true;
    		CallEqualZero.RHS.Add(InputTerm);
    		CallEqualZero.RHS.Add(LiteralZeroTerm);
    
    		IfPositive.TargetLabel = &CallEqualZero;
    
    		// Statement 5: GotoIfNot(BoolTerm)
    		FBlueprintCompiledStatement& IfZero = Context.AppendStatementForNode(Node);
    		IfZero.Type = KCST_GotoIfNot;
    		IfZero.LHS = BoolTerm;
    		Context.GotoFixupRequestMap.Add(&IfZero, NegativePin);
    
    		// Statement 6: 执行 Zero Pin
    		FBlueprintCompiledStatement& ExecZero = Context.AppendStatementForNode(Node);
    		ExecZero.Type = KCST_UnconditionalGoto;
    		Context.GotoFixupRequestMap.Add(&ExecZero, ZeroPin);
    
    	}
    

    附:class UBPNode_TriGate源代码

    • BPNode_TriGate.h
    #pragma once
    
    #include "CoreMinimal.h"
    #include "K2Node.h"
    #include "BPNode_TriGate.generated.h"
    
    UCLASS()
    class MYBLUEPRINTNODEEDITOR_API UBPNode_TriGate : public UK2Node
    {
    	GENERATED_BODY()
    public:
    	// UEdGraphNode interface
    	virtual void AllocateDefaultPins() override;
    
    	virtual FText GetTooltipText() const override { return FText::FromString(TEXT("TriGate by Integer")); }
    	virtual FText GetNodeTitle(ENodeTitleType::Type TitleType) const override { return FText::FromString(TEXT("TriGate")); }
    	// End of UEdGraphNode interface
    
    	// UK2Node interface
    	virtual FText GetMenuCategory() const { return FText::FromString(TEXT("MyBlueprintNodes")); }
    	virtual void GetMenuActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const override;
    	virtual class FNodeHandlingFunctor* CreateNodeHandler(class FKismetCompilerContext& CompilerContext) const;
    	// End of UK2Node interface
    };
    
    • BPNode_TriGate.cpp
    #include "BPNode_TriGate.h"
    #include "EdGraphSchema_K2.h"	// BlueprintGraph
    #include "BlueprintNodeSpawner.h"	// BlueprintGraph
    #include "BlueprintActionDatabaseRegistrar.h"	// BlueprintGraph
    #include "BPTerminal.h"
    #include "KismetCompilerMisc.h"
    #include "KismetCompiler.h"
    #include "Kismet/KismetMathLibrary.h"
    #include "EdGraphUtilities.h"
    
    namespace TriGatePN
    {
    	FName Input = TEXT("Integer");
    	FName Positive = TEXT("Positive");
    	FName Zero = TEXT("Zero");
    	FName Negative = TEXT("Negative");
    }
    
    class FKCHandler_TriGate : public FNodeHandlingFunctor
    {
    protected:
    	// 为每个Node开辟一个临时bool变量,用来存储输入参数的比较结果
    	TMap<UEdGraphNode*, FBPTerminal*> BoolTermMap;
    	FBPTerminal* LiteralZeroTerm;
    
    public:
    	FKCHandler_TriGate(FKismetCompilerContext& InCompilerContext)
    		: FNodeHandlingFunctor(InCompilerContext)
    	{
    	}
    
    	virtual void RegisterNets(FKismetFunctionContext& Context, UEdGraphNode* Node) override
    	{
    		FNodeHandlingFunctor::RegisterNets(Context, Node);
    
    		FBPTerminal* BoolTerm = Context.CreateLocalTerminal();
    		BoolTerm->Type.PinCategory = UEdGraphSchema_K2::PC_Boolean;
    		BoolTerm->Source = Node;
    		BoolTerm->Name = Context.NetNameMap->MakeValidName(Node) + TEXT("_CmpResult");
    		BoolTermMap.Add(Node, BoolTerm);
    
    		LiteralZeroTerm = Context.CreateLocalTerminal(ETerminalSpecification::TS_Literal);
    		LiteralZeroTerm->bIsLiteral = true;
    		LiteralZeroTerm->Type.PinCategory = UEdGraphSchema_K2::PC_Int;
    		LiteralZeroTerm->Name = TEXT("0");
    	}
    
    	virtual void Compile(FKismetFunctionContext& Context, UEdGraphNode* Node) override
    	{
    		UBPNode_TriGate* MyNode = CastChecked<UBPNode_TriGate>(Node);
    
    		// 查找输入的那个整数的Pin对应的Terminal
    		UEdGraphPin* InputPin = Context.FindRequiredPinByName(Node, TriGatePN::Input, EGPD_Input);
    
    		UEdGraphPin* PinToTry = FEdGraphUtilities::GetNetFromPin(InputPin);
    		FBPTerminal** pInputTerm = Context.NetMap.Find(PinToTry);
    		if (pInputTerm == nullptr)
    		{
    			CompilerContext.MessageLog.Error(TEXT("FKCHandler_TriGate: Failed to resolve term passed into"), InputPin);
    			return;
    		}
    
    		FBPTerminal* InputTerm = *pInputTerm;
    
    		// 查找三个输出Pin
    		UEdGraphPin* PositivePin = MyNode->FindPin(TriGatePN::Positive, EGPD_Output);
    		UEdGraphPin* ZeroPin = MyNode->FindPin(TriGatePN::Zero, EGPD_Output);
    		UEdGraphPin* NegativePin = MyNode->FindPin(TriGatePN::Negative, EGPD_Output);
    
    		// 临时bool变量的Terminal
    		FBPTerminal* BoolTerm = BoolTermMap.FindRef(MyNode);
    
    		UClass* MathLibClass = UKismetMathLibrary::StaticClass();
    		UFunction* CreaterFuncPtr = FindField<UFunction>(MathLibClass, "Greater_IntInt");
    		UFunction* EqualFuncPtr = FindField<UFunction>(MathLibClass, "EqualEqual_IntInt");
    
    		// Statement 1: 计算表达式 BoolTerm = Interger > 0
    		FBlueprintCompiledStatement& CallCreaterZero = Context.AppendStatementForNode(MyNode);
    		CallCreaterZero.Type = KCST_CallFunction;
    		CallCreaterZero.FunctionToCall = CreaterFuncPtr;
    		CallCreaterZero.LHS = BoolTerm;
    		CallCreaterZero.RHS.Add(InputTerm);
    		CallCreaterZero.RHS.Add(LiteralZeroTerm);
    
    		// Statement 2: if(BoolTerm)
    		FBlueprintCompiledStatement& IfPositive = Context.AppendStatementForNode(Node);
    		IfPositive.Type = KCST_GotoIfNot;
    		IfPositive.LHS = BoolTerm;
    
    		// Statement 3: 执行 Positive Pin
    		FBlueprintCompiledStatement& ExecPositive = Context.AppendStatementForNode(Node);
    		ExecPositive.Type = KCST_UnconditionalGoto;
    		Context.GotoFixupRequestMap.Add(&ExecPositive, PositivePin);
    
    		// Statement 4: 计算表达式 BoolTerm = Interger == 0
    		FBlueprintCompiledStatement& CallEqualZero = Context.AppendStatementForNode(MyNode);
    		CallEqualZero.Type = KCST_CallFunction;
    		CallEqualZero.FunctionToCall = EqualFuncPtr;
    		CallEqualZero.LHS = BoolTerm;
    		CallEqualZero.bIsJumpTarget = true;
    		CallEqualZero.RHS.Add(InputTerm);
    		CallEqualZero.RHS.Add(LiteralZeroTerm);
    
    		IfPositive.TargetLabel = &CallEqualZero;
    
    		// Statement 5: GotoIfNot(BoolTerm)
    		FBlueprintCompiledStatement& IfZero = Context.AppendStatementForNode(Node);
    		IfZero.Type = KCST_GotoIfNot;
    		IfZero.LHS = BoolTerm;
    		Context.GotoFixupRequestMap.Add(&IfZero, NegativePin);
    
    		// Statement 6: 执行 Zero Pin
    		FBlueprintCompiledStatement& ExecZero = Context.AppendStatementForNode(Node);
    		ExecZero.Type = KCST_UnconditionalGoto;
    		Context.GotoFixupRequestMap.Add(&ExecZero, ZeroPin);
    	}
    };
    
    void UBPNode_TriGate::AllocateDefaultPins()
    {
    	CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Exec, UEdGraphSchema_K2::PN_Execute);
    	CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Int, TriGatePN::Input);
    
    	CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Exec, TriGatePN::Positive);
    	CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Exec, TriGatePN::Zero);
    	CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Exec, TriGatePN::Negative);
    
    }
    
    void UBPNode_TriGate::GetMenuActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const
    {
    	UClass* ActionKey = GetClass();
    
    	if (ActionRegistrar.IsOpenForRegistration(ActionKey))
    	{
    		UBlueprintNodeSpawner* NodeSpawner = UBlueprintNodeSpawner::Create(GetClass());
    		check(NodeSpawner != nullptr);
    
    		ActionRegistrar.AddBlueprintAction(ActionKey, NodeSpawner);
    	}
    }
    
    FNodeHandlingFunctor * UBPNode_TriGate::CreateNodeHandler(FKismetCompilerContext & CompilerContext) const
    {
    	return new FKCHandler_TriGate(CompilerContext);
    }
    
    
    展开全文
  • Unreal蓝图入门 节点

    2019-12-21 16:49:45
    最近开始摸索Unreal蓝图基础使用,一些基本的操作大家可以在官方网站中的教程中找到,所以我们直接跳到事件图表上来讲。 首先,Unreal有丰富的注释功能,按住键盘C可以创建出注释区域,在右侧的“细节”面板我们...

    最近开始摸索Unreal的蓝图基础使用,一些基本的操作大家可以在官方网站中的教程中找到,所以我们直接跳到事件图表上来讲。
    首先,Unreal有丰富的注释功能,按住键盘C可以创建出注释区域,在右侧的“细节”面板我们可以轻松调整颜色以及气泡的出现。

    节点

    见过一些别人编辑好的蓝图的同学们都知道,蓝图中有错综复杂的连线,这些连线从一个地方出来,到另一个地方去,控制了流程和数据的输入输出。就像是交通线,每个城市就相当于“节点”

    节点类型
    事件(Events)、自定义事件、函数调用、事件调度器、流程控制、数组节点、时间轴、数学表达式节点、随机流、注释。这些都属于节点类型,丰富的节点类型可以帮助我们在小型的游戏开发中基本不需要编写自己的C++代码,实现一些功能,但是同时者考验来蓝图编写者的逻辑思维能力。

    事件
    在蓝图中可以定义各种事件,创建蓝图时默认就会为我们提供三种事件:BeginPlay Tick以及Overlap。对应着我们游戏开始,帧调用以及碰撞触发事件。 在一个蓝图中,事件的种类可以有多个,但是同种类型的事件只能有一个。
    另外,我们也可以自定义事件,Add Event一个事件,然后我们在细节面板可以修改自定义事件的名字,定义各种事件的成员变量。

    函数调用
    函数的调用是蓝图中同样重要的一环,函数调用可以简单分为一般函数调用,其他函数调用和纯函数调用。
    所谓一般函数调用,是可以在蓝图中形成的操作,同属于目标Actor或对象的函数相对应 。
    其他函数调用,是属于除了该蓝图之外的其他对象或者Actor的函数。
    纯函数调用,是可以执行的特殊动作,它不会影响世界或者世界中的对象。比如输出一个属性值,进行数学运算等。

    流程控制
    switch:提供了各种switch判断的类型,也可以自定义枚举类型在这里判断。
    branch:相当于if else语句
    Do N:接受条件执行,但是有次数限制,达到次数上限后,再次接受相同条件无法执行后续节点操作。但是可以通过重置使次数归0 .
    DoOnce:同上,但是只能执行一次
    DoOnceMultiInput:可以根据不同情况执行不同的操作,但是如果想要再次执行,需要Reset。
    FlipFlop:交替执行A和B后面的内容。
    ForLoop:for循环
    ForLoopWithBreak:可以在一定条件下终止循环,执行相应的语句。
    Gate:门,顾名思义。可以设置Enter的条件和Exit的执行语句,可以设置open的条件,close的条件,或者用toggle使其自动open和close·······
    MultiGate:可以又多种输出的开关门,可以设置开始和重置,可以选择是否随机或者循环,可以从只当index的输出开始
    Sequence:按顺序一次执行,但是值得注意的是,即使写了Delay,Sequence也会直接执行后面的语句

    变量

    存放一个值或者引用世界中一个Object或Actor的属性。
    变量类型:
    在这里插入图片描述
    在这里插入图片描述

    详细介绍:

    布尔值(bool):小案例:利用不布尔值设置一个point light的intensity,按下键盘一个键后,如果灯是打开则关闭,如果关闭则打开。
    易犯错误:将一个布尔值取反后,忘记重新把这个取反后的值设置回这个变量中,导致只能发生一次变化。
    字节(byte):学过一定计算机理论的知道,1字节是8位,在UE中存储范围是0-255。在右侧细节栏中可以设置UI中滑动的范围以及值的取值范围,后者控制前者。
    整型和64位整型:整型是32位,两者仅仅范围不同
    浮点型(float):范围很大,案例:做一个四舍五入的功能(利用浮点型转整型操作)。
    Name,String以及Text:Name是轻量级的字符串,经常被用于对象的命名 string是我们通常用的字符串,支持字符串的拼接,查找等其他语言中都存在的操作 Text是大段文本,如果游戏中出现大段对话,基本我们就使用Text,这样会对我们的本地化非常有帮助。
    Vector:引擎中很多属性例如位置,旋转,缩放都是使用Vector。案例:使用Vector修改物体的位置和缩放。
    Rotator:和Vector类似,可以用来定义一个旋转角度。
    Transform:Transform即物体的位置,旋转和缩放,第一个和第三个是Vector类型,第二个是Rotator类型。案例:获取物体的Transform中的旋转进行输出。

    Enum枚举类型:我们可以在内容浏览器中新建一个自己的枚举类型,为其设置各种情况以及解释
    Structure结构体:我们可以在内容浏览器中新建一个结构体,类似于各种编程语言中的struct
    综合案例:创建一个学生结构体,属性为姓名,年龄,以及学校(利用枚举类型),在蓝图中实现这样的功能:根据学生年龄的大小,输出“姓名”+是否成年+“(学校类型)”。

    数组节点:
    由于本篇文章是基于有一定其他编程语言经验的人编写,所以数组方面可能不会那么详尽的去介绍。
    在这里插入图片描述
    在这个地方可以修改变量的类型,有普通变量,数组,集以及字典。

    数组中的方法:
    在这里插入图片描述
    很多方法都是其他语言中存在的一些常用的方法,大多都属于增删改查,具体如何使用各位客官可以自己摸索。

    集合:
    接下来就是我们的第三种数据存储容器,集合
    在这里插入图片描述
    在C#中,我们有数组,列表,字典,哈希表,栈和队列等数据结构,集合也是一种类数据结构容器,在初等数学中我们应该学过集合相关的知识。
    如果硬要对比的话,集合就好比一个键值对相同的字典。
    在蓝图中,可以对集合进行如上图的操作。其中也包含了最常用的差并交三个基本操作。

    地图:
    第四种数据存储容器:map
    这个就很熟悉了,有的编程语言中它叫map,有的中是Dictionary,本质上就是一个键值对,而且是一种引用类型的键值对;与之相对的是,哈希表是一种值类型的键值对。
    在这里插入图片描述
    上面是map中一些常用的方法,有一定数据结构知识的客官们应该能轻松应用这些方法。

    函数(Function)

    在这里插入图片描述
    在上图中,我们已经大概了解了图标和变量的含义,那么接下来是函数。
    在这里插入图片描述
    可以覆盖,但由于我们还没有继承的子类或者父类,我们就先新建一个函数
    在这里插入图片描述
    点击新建的函数,可以进入我们的函数界面,在右侧的细节面板有我们的一些函数相关的设置,例如一些注释,访问方式(public protected private) 是否为纯函数 在输入和输出中,我们可以定义相应的函数的输入和输出数据流。

    实操训练:对一个数组进行排序
    unreal蓝图中实现数组的排序是非常消耗性能且复杂的。

    宏:
    宏跟函数非常类似,可以定义输出和输出,而它比函数多了一个功能,它可以在右侧的细节面板设置多个输入和输出引脚,有点类似于我们的分支,具体如何使用到之后继续总结。

    数学节点–蓝图中的math类
    在这里插入图片描述
    如图所示,在蓝图中搜索math,会出现很多一些蓝图中的数学节点,这些节点提供了我们对一些数据类型的基本的操作。
    数学节点也包括对transformation的操作,另外,我们的Random节点也归在数学节点里,不再做进一步的介绍,请各位客官尝试使用一下各种数学函数,灵活运用到自己的项目中。

    时间轴:
    Timeline为我们提供了一个基于时间的控制器,不仅仅是做动画,Timeline代表了一种在时间轴上进行各种操作的编程习惯。
    对Timeline的介绍分为两部分,首先是Timeline的曲线编辑器:
    在这里插入图片描述最上面一排,可以添加基于各种类型变量的曲线,设置曲线的时间,自动播放,循环等属性。在一个Timeline中可以定义多条曲线,都会在事件图表中显示出来。中间是我们的曲线编辑窗口,我们可以按shift去添加关键帧,用delete去删除关键帧。右键点击关键帧,可以设置关键帧在此处的状态是平滑,线性还是剧变等等状态。

    然后是事件图表中的Timeline节点:
    在这里插入图片描述
    Play:播放
    Play from Start:从头开始播放
    Stop:停止
    Reverse:从后往前播放
    Reverse from End:从最后往前播放
    Set New Time:为动画设置一个新的时间
    Update:每帧执行
    Finished:结束执行
    Direction:播放是从后往前还是从前往后

    Timeline在创建后,在左侧会以组件的形式出现,我们可以像对其他变量和组件一样对它进行get 和set
    下面是关于Timeline节点的一些方法:
    在这里插入图片描述

    展开全文
  • UE4之蓝图接口

    万次阅读 2018-09-10 08:18:06
    首先创建一个蓝图接口 修改蓝图接口里默认的那个函数 下边的Float是在蓝图接口中自己添加的参数 被调用的Actor要继承一下蓝图接口(工具栏找类设置) 被调用的Actor中的蓝图这样写, 获取蓝图接口,后边随便写...

    蓝图接口的好处是没有对象的限制,
    无论什么类型,只要是继承了蓝图接口
    就可以被调用

    蓝图接口的作用就是可以通过一个类调用另一个类的事件
    也就是可以起到桥梁的作用
    这里写图片描述
    首先创建一个蓝图接口
    修改蓝图接口里默认的那个函数
    这里写图片描述
    下边的Float是在蓝图接口中自己添加的参数
    这里写图片描述

    被调用的Actor要继承一下蓝图接口(工具栏找类设置)
    这里写图片描述

    被调用的Actor中的蓝图这样写,
    获取蓝图接口,后边随便写点功能,

    这里写图片描述

    然后在调用着的蓝图里这样写
    这里写图片描述
    意思是获取所有继承这个蓝图接口的actor,然后第0个,调用他的事件,
    很简单,

    这样吧两个Actor都拉到关卡中按下“2”发现还是无法调用,是因为调用者没有获取按键,方法很简单,获取玩家控制,启用按键即可
    这里写图片描述

    这样在调用者传送参数,被调用的Actor就会输出了
    这里写图片描述

    展开全文
  • 通过派生class UK2Node,为蓝图添加自定义节点;这篇博客我们先实现一个最简单的自定义节点,下篇文章将完成“动态添加输入Pin”的蓝图节点。

    前面一篇文章中介绍了扩展蓝图的几种方法,其中最灵活的就是自定义K2Node了,也就是通过定义一个class UK2Node的派生类来给蓝图添加新的节点。这种方法开发起来一起是最麻烦的,一般只有在需要“动态增加Pin”的时候才使用这种方式。

    下面我们就一步一步的来实现一个最简单的“Hello World蓝图节点”,通过这个节点我们来看看扩展K2Node的关键步骤。在后面一篇博客再去实现“动态增加输入项”的节点

    项目创建

    我们创建一个C++: Basic Code工程,然后,手动添加一个Editor模块,如下图所示:
    在这里插入图片描述
    手动添加这个模块的操作如下:

    1. 修改 .uproject 文件,在Modules字段,添加新的项目,命名为:MyBlueprintNodeEditor,Type设置为:Editor;
    2. 在Source目录下建立一个子目录,命名为: MyBlueprintNodeEditor,这个子目录就是保存这个模块的所有源文件了;
    3. 在这个子目录下创建一个:MyBlueprintNodeEditor.Build.cs,用来实现UBT对这个模块的build的配置;并把“BlueprintGraph”模块添加的PrivateDependencyModuleNames中;
    4. 在“MyBlueprintNodeEditor.Target.cs”文件中添加这个模块:
    ExtraModuleNames.AddRange( new string[] {
              "MyBlueprintNode",
              "MyBlueprintNodeEditor"
          } );
    

    为什么要添加一个Editor模块呢?因为class UK2Node这个类属于一个名为“BlueprintGraph”的Editor模块,而蓝图的Graph编辑,Compile之后生成一个class UBlueprintGeneratedClass的实例,而UK2Node这些对象的实例在Runtime是不需要的。

    创建新的节点类型

    首先,我们来创建一个class UK2Node的派生类,命名为:class UBPNode_SayHello

    UCLASS()
    class MYBLUEPRINTNODEEDITOR_API UBPNode_SayHello : public UK2Node
    {
    	GENERATED_BODY()
    	
    };
    

    有了这个类之后,在Blueprint编辑器里面还不能创建它。我们需要把这个节点的创建操作添加到右键菜单中,并且稍微美化一下这个节点的显示,通过以下几个函数来完成。

    UCLASS()
    class MYBLUEPRINTNODEEDITOR_API UBPNode_SayHello : public UK2Node
    {
    	GENERATED_BODY()
    
    public:
    	// UEdGraphNode interface
    	virtual FText GetTooltipText() const override { return FText::FromString(TEXT("a hello world node")); }
    	virtual FText GetNodeTitle(ENodeTitleType::Type TitleType) const override { return FText::FromString(TEXT("Say Hello World")); }
    	// End of UEdGraphNode interface
    
    	// UK2Node interface
    	virtual FText GetMenuCategory() const { return FText::FromString(TEXT("MyBlueprintNodes")); }
    	virtual void GetMenuActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const override;
    	// End of UK2Node interface
    };
    

    这几个函数的名字已经足够说明它们的作用了,其中最重要的就是“GetMenuActions()”这个了,它的实现代码如下:

    void UBPNode_SayHello::GetMenuActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const
    {
    	UClass* ActionKey = GetClass();
    
    	if (ActionRegistrar.IsOpenForRegistration(ActionKey))
    	{
    		UBlueprintNodeSpawner* NodeSpawner = UBlueprintNodeSpawner::Create(GetClass());
    		check(NodeSpawner != nullptr);
    
    		ActionRegistrar.AddBlueprintAction(ActionKey, NodeSpawner);
    	}
    }
    

    以上简单代码之后,我们就可以通过右键菜单创建一个空的节点了,如下图所示:
    在这里插入图片描述

    实现节点连接

    接下来,需要把这个Node连接到整个Graph,才能使它能够被调用。我们这个节点将通过Pin来连接到整个蓝图(Pure Node没有Pin)。通过override父类的AllocateDefaultPins()函数,我们来添加需要的两个pin:

    void UBPNode_SayHello::AllocateDefaultPins() {
    
    	CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Exec, UEdGraphSchema_K2::PN_Execute);
    	CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Exec, UEdGraphSchema_K2::PN_Then);
    }
    

    我们添加了一个Input Pin、一个Output Pin,实现效果如下图所示:
    在这里插入图片描述

    实现节点的功能

    上面这个节点编译就会报错,因为我们还没有实现最核心的蓝图节点编译函数。在这里,我们通过重载“ExpandNode()”来实现这个节点的功能。我个人目前理解这个函数的名字含义是:通过把这个节点展开成一个已有节点类型组成的Sub Graph来实现本节点的功能。先看代码吧:

    void UBPNode_SayHello::ExpandNode(FKismetCompilerContext & CompilerContext, UEdGraph * SourceGraph) {
    	Super::ExpandNode(CompilerContext, SourceGraph);
    
    	UEdGraphPin* ExecPin = GetExecPin();
    	UEdGraphPin* ThenPin = GetThenPin();
    	if (ExecPin && ThenPin) {
    
    		// create a CallFunction node
    		FName MyFunctionName = GET_FUNCTION_NAME_CHECKED(UMyBlueprintFunctionLibrary, SayHello_Internal);
    
    		UK2Node_CallFunction* CallFuncNode = CompilerContext.SpawnIntermediateNode<UK2Node_CallFunction>(this, SourceGraph);
    		CallFuncNode->FunctionReference.SetExternalMember(MyFunctionName, UMyBlueprintFunctionLibrary::StaticClass());
    		CallFuncNode->AllocateDefaultPins();
    
    		// move pins
    		CompilerContext.MovePinLinksToIntermediate(*ExecPin, *(CallFuncNode->GetExecPin()));
    		CompilerContext.MovePinLinksToIntermediate(*ThenPin, *(CallFuncNode->GetThenPin()));
    	}
    
    	// break any links to the expanded node
    	BreakAllNodeLinks();
    }
    

    这段代码通过新建一个UK2Node_CallFunction节点,然后把Pin重新绑定来实现所需的功能;UK2Node_CallFunction节点可以调用任意的UFUNCTION,在这里我们调用一个自己的简单函数:UMyBlueprintFunctionLibrary::SayHello_Internal(),这个函数实现很简单,需要注意的是在它的UFUNCTION声明里面,我知道了它为“BlueprintInternalUseOnly”,具体看代码:

    UCLASS()
    class MYBLUEPRINTNODE_API UMyBlueprintFunctionLibrary : public UBlueprintFunctionLibrary
    {
    	GENERATED_BODY()
    
    public:
    	UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true"))
    	static void SayHello_Internal();
    };
    

    结束语

    上面这个例子的完整UE4 Project和源代码都已经上传到我的GitHub供大家参考:https://github.com/neil3d/UnrealCookBook/tree/master/MyBlueprintNode

    通过这个例子,我们已经掌握了最基本的K2Node扩展的方式,其实很简单,主要是重写父类class UK2Node以及父类的父类class UEdGraphNode的一些关键方法。当然,目前这个节点和标准的UFUNCTION节点没什么差别,在下一篇博客中我们将实现一个动态分配Pin的节点,那才能体现这种方法的价值。

    展开全文
  • UE4 蓝图教程(四) UI

    千次阅读 2018-04-26 22:11:04
    在虚幻引擎4中你可以用Unreal Motion Graphics(UMG)创建UI。UMG允许您通过拖拽UI元素,例如拖拽按钮和文本标签快捷的构建UI。在本教程中,您将学习如何:1,创建显示信息标签(HUD),显示计数和时间。2,显示HUD3,...
  • UE4蓝图:创建和使用结构体

    千次阅读 2018-03-18 13:43:48
    如果我们想在蓝图创建一个类,或者一个对象来作为一个整体,保存一些,一系列的属性,而只是单纯的修改和获取它的值,那么这就是结构体Struct. 那么在蓝图中怎么去创建一个结构体类型呢?当我们创建一个变量后,可以在...
  • 这篇博客主要是深入理解蓝图整个流程的的底层机制,包括节点编辑、编译、字节码解释执行。理解了这些,对前面几篇所讲的蓝图扩展,可以有一个更清晰的认识。
  • 通过派生class UK2Node和class SGraphNodeK2Base,为蓝图添加自定义节点,实现一个“动态添加输入Pin”的蓝图节点。
  • Unreal Engine 4蓝图可视化编程游戏引擎(例如虚幻引擎4)作为强大的商业游戏的制作工具,越来越受传统游戏工作室以外的新老游戏开发者所欢迎。虚幻引擎为过去10年中发布的许多最受欢迎的控制台和PC游戏提供了动力,...
  • 本节书摘来异步社区《Unreal Engine 4蓝图可视化编程》一书中的第1章,第1.1节,作者: Brenden Sewell 译者: 陈东林 责编: 胡俊英,更多章节内容可以访问云栖社区“异步社区”公众号查看。 第1章 使用蓝图进行...
  • 学习心得,做下笔记,防止忘~ 1.新建关卡(level) 2.新建蓝图类 3.打开蓝图类 ...4.把“”“视口”右下角的input 中的Auto Receive input 设置为“”“player 0”,如下图: ...创建成功关系图后一定要
  • 本节书摘来异步社区《Unreal Engine 4蓝图可视化编程》一书中的第1章,第1.3节,作者: Brenden Sewell 译者: 陈东林 责编: 胡俊英,更多章节内容可以访问云栖社区“异步社区”公众号查看。 1.3 材质 之前我们...
  • 本节书摘来异步社区《Unreal Engine 4蓝图可视化编程》一书中的第1章,第1.7节,作者: Brenden Sewell 译者: 陈东林 责编: 胡俊英,更多章节内容可以访问云栖社区“异步社区”公众号查看。 1.7 小结 本章通过UE4...
  • 本节书摘来异步社区《Unreal Engine 4蓝图可视化编程》一书中的第2章,第2.4节,作者: Brenden Sewell 译者: 陈东林 责编: 胡俊英,更多章节内容可以访问云栖社区“异步社区”公众号查看。 2.4 小结 游戏中添加...
  • 本节是为Lazer创建蓝图,但不是简单的创建蓝图那么简单。 首先从骨骼创建一个Anim Composite。 创建的文件一会要用到,再从SekeletalMesh文件创建一个动画蓝图,运用刚才创建好的文件按如下连接,我们打算改变...
  • Unreal Engine 4 蓝图之自定义事件

    千次阅读 2015-01-11 01:15:00
    UE4的蓝图就跟C++等编程语言在概念上是非常类似的。在蓝图中你可以定义变量、函数、宏等等,高级点的,它还可以被继承。这还不算,我们还可以定义蓝图接口,规范子类的行为。基本上C++中可以做的,蓝图也可以做到,...
  • 引擎中提供两种蓝图异步节点的实现方式,这里我们主要介绍 Blueprint Async Action 的实现方式。
  • 虚幻4蓝图快速入门(一)

    万次阅读 2018-06-02 19:48:12
    蓝图快速入门什么是蓝图虚幻引擎中的蓝图可视化系统是一个完整的游戏脚本系统,其理念是使用基于节点的界面从虚幻编辑器中创建游戏可玩性元素,该系统非常灵活且非常强大,因为它为设计人员提供了一般仅供程序员使用...
  • 方式一: UPROPERTY(EditAnywhere) TSubclassOf<class APickupAndRotateActor> ObjectToSpawn; FSCPSceneItemsDataStruct scene_item = SCPGAMEINSTANCE(this)->JsonStaticDataManager->...
  • 本节书摘来异步社区《Unreal Engine 4蓝图可视化编程》一书中的第1章,第1.4节,作者: Brenden Sewell 译者: 陈东林 责编: 胡俊英,更多章节内容可以访问云栖社区“异步社区”公众号查看。 1.4 创建第一个蓝图 ...
1 2 3 4 5 ... 20
收藏数 1,384
精华内容 553
关键字:

unreal 创建蓝图