【UEC++】虚幻C++基础入门

温馨提示:懒得分P了,建议配合目录使用(目录下没有小标题的就是还没更完的)

(由于此篇非一次性创造完成,缝缝补补中难免造成案例图片的版本不尽相同,可能上一章节中使用 5.0 配合 VS,下一章节中又变成 4.27 + Rider 的情况,还请谅解~ 软件都是工具,重在体会精神)

 

UEC++项目环境的搭建

① Visual Studio + Visual assists X(小番茄,但耗性能)

② Rider for UE(学生可申请免费使用,请务必白嫖,好用的不得鸟)

③ Visual Studio Code


 

一. 虚幻工程目录结构与代码命名规则

1.1 工程目录结构

  • 打包项目时,主要保住下面3个红框文件夹,其他的会再生成不用担心

 

  •   .vs:编译器 VS 的环境配置文件
  • Binaries:存放编译生成的结果二进制文件,编译动态库,如果热加载出现问题可以删除后重新生成(SVN 可以 ignore,反正每次都会生成)
  • Config:配置文件
  • Content:所有的资源和蓝图等都放在该目录里
  • DerivedDataCache:“DDC”,存储着引擎针对平台特化后的资源版本,过程文件(SVN可以ignore,比如同一个图片,针对不同的平台有不同的适合格式,这个时候就可以在不动原始的uasset的基础上,比较轻易的再生成不同格式资源版本)
  • Intermediate:中间文件,存放着一些临时生成的文件(SVN 可以 ignore)
    • Build 的中间文件,.obj 和预编译头等
    • UHT 预处理生成的 .generated.h/.cpp 文件
    • VS.vcxproj 项目文件,可通过 .uproject 文件生成编译生成的 Shader 文件
    • AssetRegistryCache:Asset Registry 系统的缓存文件,Asset Registry 可以简单理解为一个索引了所有 uasset 资源头信息的注册表,CachedAssetRegistry.bin 文件也是如此
  • Saved:存储自动保存文件,其他配置文件,日志文件,引擎崩溃日志,硬件信息,烘培信息数据等(Svn 可以 ignore)
  • Source:代码文件,存放工程源码。

 

1.2 项目目录结构

 

  • Engine 引擎源码文件(只读)
    • 虚幻是开源项目,我们可以直接在工程中看到引擎源码但无法修改(如需修改请下载 Git 源码工程)
    • 引擎源码:Engine - Source - Runtime
    • 编辑器源码:Engine - Source - Editor
  • Games 项目工程文件(主要编写逻辑文件)
    • 我们的代码需要在此工程中编写。虚幻中采用了编译模块方式进行引擎构建,所以对于引擎来说,我们编写的内容只是一个模块,模块会被动态编译为库文件,加载到引擎中使用
    • Target.cs 文件就是模块配置文件
  • Visualizers 虚幻 4.21 加入的文件夹,VS 编辑器配置文件
  • 当添加、删除、重命名 C++类、改变 C++ 源码文件夹结构后,需要重新生成

 

1.3 编译选项

  • 默认是 DevelopmentEditor,但可以根据项目开发的实际情况自选

 

  • 每种编译配置包含两种关键字:
    • 引擎以及游戏项目的状态
    • 正在编译的目标

 

  • 为了调试代码方便,一般选择 DebugEditor 来加载游戏项目
  • 如需最简化流程时,可用 Debug 来运行独立版本
  • 打包时,打 Shipping 包
  • Debug 模式下可查看的性能信息更多,但会牺牲一些 CPU 和 GPU 的时间
  • Development(默认)性能会稍优一些

 

 

  • 整合如下:

 

  • 三种编译方式
    • 热编译 Hot reload
      • VS 和 UE 未连接,最简单,但偶尔会出现问题(编写代码后在 UE 点击“编译”)
    • 从 VS 运行
      • VS 和 UE 连接,彻底、问题少、可进行代码调试(编译器中 F5 编译,每次启动,编译器中:Debug - Detach 恢复到 Hot reload)
    • cmd 命令行
      • 最 geek 的一种方式 

 

1.4 编译系统:UBT和UHT

  • 我们写的 UE4 代码不是标准的 C++ 代码,是基于 UE4 源代码层层改装了很多层的魔改 C++。
  • UBT(Unreal Build Tool,C#):UE4 的自定义工具,来编译 UE4 的逐个模块并处理依赖
  • UHT(Unreal Header Tool,C++):UE4 的 C++ 代码解析生成工具
  • UHT 将 UE 代码转换成标准的 C++ 代码,而 UBT 负责调用 UHT 来实现这个转化工作,转化完之后 UBT 调用标准的 C++ 代码的编译器来将 UHT 转化后的标准 C++ 代码完成编译成二进制文件
  • 整体上看,UHT 是 UBT 的编译流程的一部分

 

1.5 代码命名规则(遵循帕斯卡命名法)

虚幻引擎|的史诗C++编码标准虚幻引擎5.1文档 (unrealengine.com)

UHT 在工作的时候需要你提供正确的前缀:

  • 模版类:T 前缀(如 TArray,TMap,TSet)
  • UObject 派生类:U前缀
  • AActor 派生类:A 前缀
  • SWidget 派生类:S 前缀
  • 全局对象:G 开头(如 GEngine)
  • 抽象接口:I 前缀
  • 枚举:E 开头
  • bool 变量:b 前缀
  • 其他的大部分以 F 开头(如FString,FName)
  • typedef 的以原型名前缀为准(如 typedef TArray FArrayOfMyTypes)
  • 在编辑器里和 C# 里,类型名是省略掉前缀的

 

1.6 资源命名规则

 

1.7 文件夹命名规则

 

 

二. Actor

2.1 创建 Actor

  • Actor 是载体,放在Actor 上的东西称之为组件 Component(每一个 Actor 在场景中都不具备存在的能力,但添加了场景组件 SceneComponent,就可以在场景中存在)
  • 创建 Actor 的方式:
    • ① 静态创建:直接在场景中编辑拖拽
      • 优势:创建由引擎构建场景时进行创建,无需编码,更加直观简单
      • 缺点:可能会影响游戏启动速度,增加场景构建负担
    • ② 动态创建:通过编码动态进行生成
      • 优势:可控性更强,动态生成的 Actor 会持有有效的操作指针,可根据实际情况生成,更加灵活
      • 缺点:难,复杂

 

  1. 通过 SpawnActor() 函数动态创建(基础入门版):
    • 创建 C++ 文件

       

    • 打开后,可以看到虚幻帮我们写好了的内容结构

    • 重写 BeginPlay() 函数和 Tick() 函数:头文件 .h 中写函数声明,.cpp 文件中写函数定义

    • 用 SpawnActor 函数动态生成 Actor
    • SpawnActor 是工厂函数,需要通过 UWorld 指针 进行创建

    • 先去源码 World.h 文件中观察一下 UWorld 类中要调用的 SpawnActor() 函数,果然有很多
    • 我们先从提供了默认值的 T* SpawnActor( const FActorSpawnParameters& SpawnParameters = FActorSpawnParameters() ) 基础版开始入门

      //函数源码
      /*
      * Templated version of SpawnActor that allows you to specify a class type via the template type */ //(泛型编程)由于 C++ 是编译型语言,在编译前必须确定好类型, //定义一个还不确定具体类型的 T类型,调用时需使用尖括号来确定<具体类型> template< class T > T* SpawnActor( const FActorSpawnParameters& SpawnParameters = FActorSpawnParameters() ) { return CastChecked<T>(SpawnActor(T::StaticClass(), NULL, NULL, SpawnParameters),ECastCheckedType::NullAllowed); }

      //此处插播一下【模板函数 / 泛型函数】的原理
      //以 Sum 求和的重载函数为例:
      int Sum(int a, int b)
      {
          return a + b;
      }
      
      int Sum(float a, float b)
      {
          return a + b;
      }
      
      int Sum(double a, double b)
      {
          return a + b;
      }
      
      //调用 Sum 求和函数时,会根据传入参数的数据类型不同,调用如上不同的重载函数
      //而当,不确定传入参数的数据类型时,C++提供了 “泛型编程” 的概念,申请任意一个符号类型(如 T)来代替未来将要传入的参数数据类型(效果如上)
      template<class T>
      
      //特殊情况下,有多个未知类型,则:
      template<class T,class T1,class T2>
      T SumFun(T a, T b)
      {
          return a + b;
      }
      
      //调用模板函数 / 泛型函数时,需要使用尖括号写出明确的类型
      SumFun<int>(1,2);
      SumFun<float>(1,2);  //参数1,2会被强转为 float 类型

       

    • 在 BeginPlay() 函数定义中调用 SpawnActor() 函数
      • 由于 UWorld 本身的头文件已经在父类中被包含,所以无需额外引入头文件 #include "Engine/UWorld.h"

      • 假如需要生成自定义的 AActor,则需要引入头文件:

        //GetWorld() 函数源码
        UWorld* AActor::GetWorld() const
        {
            // CDO objects do not belong to a world
            // If the actors outer is destroyed or unreachable we are shutting down and the world should be nullptr
            if (!HasAnyFlags(RF_ClassDefaultObject) && ensureMsgf(GetOuter(), TEXT("Actor: %s has a null OuterPrivate in AActor::GetWorld()"), *GetFullName())
                && !GetOuter()->HasAnyFlags(RF_BeginDestroyed) && !GetOuter()->IsUnreachable())
            {
                if (ULevel* Level = GetLevel())
                {
                    return Level->OwningWorld;
                }
            }
            return nullptr;
        }

         

      • 生成效果如下:会在原点生成 2个空 Actor

  2. 通过 SpawnActor() 函数动态创建(进阶入门版):
    • 同理,再去源码 World.h 文件中观察一下 UWorld 类中要调用的 SpawnActor() 函数,共 5 个:

    • 现在请尝试:依次调用上面 5 个 SpawnActor() 函数

    • 其中,需要特别说明的是:
      • FVector
        • 是一个结构体,由 X,Y,Z 三个浮点变量组成
      • FRotator
        • 也是一个结构体,由 Pitch,Yaw,Roll 三个浮点变量组成
      • FTransform
        • 虽是一个结构体,但比上面的复杂亿点,由 FQuat Rotation(四元数) 、FVector Scale3D(矢量)、FVector Translation(矢量) 组成
      • UClass*
        • 每个继承 Actor 里面都有 StaticClass() ,StaticClass() 是一个合成函数,获取一个UClass指针,旨在将操作类作为一个参数进行传递(传递模版)
    • 运行后,可以明显看到生成了 5 个Actor

  3. 通过 SpawnActorDeferred() 函数动态滞后创建(进阶升级版):
    • 滞后生成,适用于占资源时的优化
    • 其中,StaticClass() 是合成函数,获取一个UClass指针,旨在将操作类作为一个参数进行传递(传递模版)

    • //SpawnActorDeferred 函数源码:
          /**
           * Spawns given class and returns class T pointer, forcibly sets world transform (note this allows scale as well). WILL NOT run Construction Script of Blueprints 
           * to give caller an opportunity to set parameters beforehand.  Caller is responsible for invoking construction
           * manually by calling UGameplayStatics::FinishSpawningActor (see AActor::OnConstruction).
           */
          template< class T >
          T* SpawnActorDeferred(
              UClass* Class,
              FTransform const& Transform,
              AActor* Owner = nullptr,
              APawn* Instigator = nullptr,
              ESpawnActorCollisionHandlingMethod CollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::Undefined
              )
          {
              if( Owner )
              {
                  check(this==Owner->GetWorld());
              }
              FActorSpawnParameters SpawnInfo;
              SpawnInfo.SpawnCollisionHandlingOverride = CollisionHandlingOverride;
              SpawnInfo.Owner = Owner;
              SpawnInfo.Instigator = Instigator;
              SpawnInfo.bDeferConstruction = true;
              return (Class != nullptr) ? Cast<T>(SpawnActor(Class, &Transform, SpawnInfo)) : nullptr;
          }
      //FinishSpawning 函数源码:
      void AActor::FinishSpawning(const FTransform& UserTransform, bool bIsDefaultTransform, const FComponentInstanceDataCache* InstanceDataCache)
      {
      #if ENABLE_SPAWNACTORTIMER
          FScopedSpawnActorTimer SpawnTimer(GetClass()->GetFName(), ESpawnActorTimingType::FinishSpawning);
          SpawnTimer.SetActorName(GetFName());
      #endif
      
          if (ensure(!bHasFinishedSpawning))
          {
              bHasFinishedSpawning = true;
      
              FTransform FinalRootComponentTransform = (RootComponent ? RootComponent->GetComponentTransform() : UserTransform);
      
              // see if we need to adjust the transform (i.e. in deferred cases where the caller passes in a different transform here 
              // than was passed in during the original SpawnActor call)
              if (RootComponent && !bIsDefaultTransform)
              {
                  FTransform const* const OriginalSpawnTransform = GSpawnActorDeferredTransformCache.Find(this);
                  if (OriginalSpawnTransform)
                  {
                      GSpawnActorDeferredTransformCache.Remove(this);
      
                      if (OriginalSpawnTransform->Equals(UserTransform) == false)
                      {
                          UserTransform.GetLocation().DiagnosticCheckNaN(TEXT("AActor::FinishSpawning: UserTransform.GetLocation()"));
                          UserTransform.GetRotation().DiagnosticCheckNaN(TEXT("AActor::FinishSpawning: UserTransform.GetRotation()"));
      
                          // caller passed a different transform!
                          // undo the original spawn transform to get back to the template transform, so we can recompute a good
                          // final transform that takes into account the template's transform
                          FTransform const TemplateTransform = RootComponent->GetComponentTransform() * OriginalSpawnTransform->Inverse();
                          FinalRootComponentTransform = TemplateTransform * UserTransform;
                      }
                  }
      
                      // should be fast and relatively rare
                      ValidateDeferredTransformCache();
                  }
      
              FinalRootComponentTransform.GetLocation().DiagnosticCheckNaN(TEXT("AActor::FinishSpawning: FinalRootComponentTransform.GetLocation()"));
              FinalRootComponentTransform.GetRotation().DiagnosticCheckNaN(TEXT("AActor::FinishSpawning: FinalRootComponentTransform.GetRotation()"));
      
              {
                  FEditorScriptExecutionGuard ScriptGuard;
                  ExecuteConstruction(FinalRootComponentTransform, nullptr, InstanceDataCache, bIsDefaultTransform);
              }
      
              {
                  SCOPE_CYCLE_COUNTER(STAT_PostActorConstruction);
                  PostActorConstruction();
              }
          }
      }
    • 运行后效果

       

 

 2.2 销毁 Actor

  • Actor 的消亡
    1.  通过 Destroy() 函数动态销毁

      //Destroy() 函数源码:
      //bNetForce:是否强制网络同步删除(默认 true)
      //bShouldModifyLevel :控制删除 actor / 修改关卡的先后顺序(默认 true:先修改关卡(把 actor 先移除出场景),再从内存中彻底删除 actor)
      bool AActor::Destroy( bool bNetForce, bool bShouldModifyLevel ) { // It's already pending kill or in DestroyActor(), no need to beat the corpse if (!IsPendingKillPending()) { UWorld* World = GetWorld(); if (World) { World->DestroyActor( this, bNetForce, bShouldModifyLevel ); } else { UE_LOG(LogSpawn, Warning, TEXT("Destroying %s, which doesn't have a valid world pointer"), *GetPathName()); } } return IsPendingKillPending(); }

       

      效果如下:

       

    2.  通过 SetLifeSpan() 函数延迟销毁(Actor 被标记为等待销毁并从关卡的Actor阵列中移除)

      //SetLifeSpan()函数源码:
      
      void AActor::SetLifeSpan( float InLifespan )
      {
          // Store the new value
          InitialLifeSpan = InLifespan;
          // Initialize a timer for the actors lifespan if there is one. Otherwise clear any existing timer
          if ((GetLocalRole() == ROLE_Authority || GetTearOff()) && !IsPendingKill())
          {
              if( InLifespan > 0.0f)
              {
                  GetWorldTimerManager().SetTimer( TimerHandle_LifeSpanExpired, this, &AActor::LifeSpanExpired, InLifespan );
              }
              else
              {
                  GetWorldTimerManager().ClearTimer( TimerHandle_LifeSpanExpired );        
              }
          }
      }

       

    3. 当对象被删除时(非内存删除)需要通过 EndPlay() 函数进行回调操作
      • 对象被彻底清除时回调 EndPlay() 函数,回调会进行删除类型通知

         

        //EndPlay()函数源码:
        //
        
        void AActor::EndPlay(const EEndPlayReason::Type EndPlayReason)
        {
            if (ActorHasBegunPlay == EActorBeginPlayState::HasBegunPlay)
            {
                TRACE_OBJECT_EVENT(this, EndPlay);
        
                ActorHasBegunPlay = EActorBeginPlayState::HasNotBegunPlay;
        
                // Dispatch the blueprint events
                ReceiveEndPlay(EndPlayReason);
                OnEndPlay.Broadcast(this, EndPlayReason);
        
                TInlineComponentArray<UActorComponent*> Components;
                GetComponents(Components);
        
                for (UActorComponent* Component : Components)
                {
                    if (Component->HasBegunPlay())
                    {
                        Component->EndPlay(EndPlayReason);
                    }
                }
            }
        }

 

 

2.3 Actor 生命周期

  • Actor 的 2 种创建方式,虽有细微差异,但是总体不大,基本分为以下调用流程:
    1. 调用构造函数
    2. 初始化成员变量
    3. 如有蓝图,则初始化蓝图数据
    4. 构建组件
    5. BeginPlay(标志着Actor被创建到世界当中)
    6. Tick
      //MyActor.cpp
      //借助打印验证如上调用流程
      
      // Fill out your copyright notice in the Description page of Project Settings.
      
      
      #include "MyActor.h"
      
      // Sets default values(构造函数内)
      AMyActor::AMyActor()
      {
           // Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
          PrimaryActorTick.bCanEverTick = true;
          UE_LOG(LogTemp,Log,TEXT("1.最先调用构造函数"));
          UE_LOG(LogTemp,Log,TEXT("2.然后初始化成员变量"));
      }
      
      // Called when the game starts or when spawned
      void AMyActor::BeginPlay()
      {
          Super::BeginPlay();
          UE_LOG(LogTemp,Log,TEXT("BeginPlay()"));
      }
      
      // Called every frame
      void AMyActor::Tick(float DeltaTime)
      {
          Super::Tick(DeltaTime);
          UE_LOG(LogTemp,Log,TEXT("Tick(DeltaTime)"));
      }

 

  • Actor 生命周期详解图:Actor 被实例的三种主要路径(无论 Actor 的创建方式如何,销毁路径均相同)
    • 静态创建
      • Load Actors from Disk 从磁盘加载
      • Play in Editor 从编辑器复制
    • 动态创建
      • SpawnActor 生成
      • SpawnActorDeferred 延迟生成
    • 销毁

 

 

 

三. 日志输出

 3.1 屏幕日志输出

  •  借助全局变量GEngine指针调用函数 AddOnScreenDebugMessage,完成屏幕输出

 

    • Key = -1时,则添加新的消息,不会覆盖旧有消息(当key为-1时,bNewerOnTop有效,直接添加到队列最上层)
    • Key不是-1时,则更新现有消息,效率更高

 

  •  打印过程:
    • 在 MyGameModeBase C++文件中重写 BeginPlay() 函数,借助全局变量 GEngine 指针调用函数 AddOnScreenDebugMessage
      //在MyGameModeBase.h 文件
      UCLASS()
      class UE_C_API AMyGameModeBase : public AGameModeBase
      {
          GENERATED_BODY()
          
      public:
          //重写 BeginPlay()函数
          virtual void BeginPlay()
      };
      //在 MyGameModeBase.cpp 文件
      
      void AMyGameModeBase::BeginPlay()
      {
          //void UEngine::AddOnScreenDebugMessage(int32 Key, float TimeToDisplay, FColor DisplayColor, const FString & DebugMessage, bool bNewerOnTop, const FVector2D & TextScale)
          GEngine->AddOnScreenDebugMessage(-1, 10, FColor::Blue, TEXT("HI"));
      }

 

    • 把  MyGameModeBase 挂载到游戏模式重载里面,再运行就可以看到打印在屏幕上的内容了:

 

    • 或者显示文字

 

3.2 控制台日志输出

  •  使用宏UE_LOG进行控制台日志输出(日志会写入本地缓存)
  • 日志分类
    • 决定了内容输入到控制台时的分类项

    •  日志类型冗余度,分为:
      • Fatal(会终止进程)致命问题

      • Error(会终止进程)错误问题

      • Warning、Display、Log(较常用的日志分类项)

      • Verbose (将日志详细信息记录到日志文档,但不向控制台输出)

      • VeryVerbose(将日志详细信息记录到日志文档,但不向控制台输出)

  • 过程演示:
    • 首先打开输出日志

 

    • 在 MyGameModeBase.cpp 文件中重写 BeginPlay()函数(和上面一样),使用宏UE_LOG进行控制台日志输出 
      void AMyGameModeBase::BeginPlay()
      {
          Super::BeginPlay();
      
          //UE_LOG(LogAutomationController, Display, TEXT("Report can be opened in the editor at '%s'"), ReportExportPath.IsEmpty() ? *FPaths::ConvertRelativePathToFull(FPaths::AutomationReportsDir()) : *ReportExportPath);
          UE_LOG(LogTemp, Log, TEXT("嘿嘿嘿hhhh"));
      }

       

    • 这样就可以在控制台打印了

 

 

3.3 自定义日志分类

  • 上面的日志LogTemp都是系统给的,开发大型项目的时候可以自己定义日志分类,方便更快捷的查询自己想要的消息
  • 使用宏进行自定义日志输出
    //-------------在MyGameModeBase.h文件:
    //1.声明日志分类(宏)
    //DECLARE_LOG_CATEGORY_EXTERN(自定义日志分类名称(Log开头), 日志默认级别(一般使用Log), 日志编译级别(高于此级别的不会被编译,一般用All));
    DECLARE_LOG_CATEGORY_EXTERN(LogProjectName, Log, All);
    
    UCLASS()
    class UE_C_API AMyGameModeBase : public AGameModeBase
    {
        GENERATED_BODY()
    
        virtual void BeginPlay();
    };
    //------------在MyGameModeBase.cpp文件:
    //2.定义日志分类
    //DEFINE_LOG_CATEGORY(CategoryName),放外面的 LogProjectName变成全局类型名
    DEFINE_LOG_CATEGORY(LogProjectName);
    void AMyGameModeBase::BeginPlay()
    {
        //3.打印到控制台和日志文件
        UE_LOG(LogProjectName, Log, TEXT("自定义日志分类"));
    }

     

  • 运行结果如下:

 

 

3.4 格式化日志

  •  通过占位符,传参输出
    • %d 整数输出

    • %f 浮点数输出

    • %s 输出UE类型字符串(非对象型字符串FString)

      //在MyGameModeBase.cpp文件:
      
      void AMyGameModeBase::BeginPlay()
      {
          int32 A = 10;  //整数参数
          float B = 3.14;  //浮点参数
          FString C = TEXT("abc");  //非对象型字符串FString
      
          //传参
          UE_LOG(LogTemp, Log, TEXT("格式化日志输出:%d,%f,%s"), A, B, *C);
      }

       

    • 运行结果如下:

 

 四. 数据类型

 4.1 基本数据类型

  • UE 对于 C++ 基本数据类型进行深度重定义,禁止在 UE 中使用 C++ 的基本数据类型,这样会影响引擎的跨平台特性
  • UE 常用数据类型
    • 无符号基本数据类型
      //UE数据类型
      bool bTest01 = true;  //true,false
      TCHAR ch = 'a';  //字符型,不要用char c1
      
      uint8 a = 10;  //无符号字节 typedef unsigned char
      uint16 a1 = 20;  //无符号短整型 typedef unsigned short
      uint32 a2 = 30;  //无符号整型 typedef unsigned int
      uint64 a3 = 40;  //无符号"四字" typedef unsigned _int64
      
      int8 b1 = -10;  //有符号字节
      int16 b2 = -20;  //有符号短整型
      int32 b3 = -30;  //有符号整型
      int64 b4 = -40;  //有符号"四字"
      
      float c = 3.14;  //单精度浮点型
      double d = 3.14;  //双精度浮点型

       

  • 类型转换:
    static_cast<int>()
    //静态转换
    
    reinterpret_cast<int>()
    //强制转换
    
    std::string str;
    int a = atoi(str.c_str());
    //将string转成char,再最终转成int



4.2 字符编码

  • 虚幻引擎4中的所有字符串都作为FStrings或TCHAR数组以UTF-16 格式存储在内存中
  • UE4也提供了如下的字符串和编码转换的宏定义(StringConv.h):

 

4.3 字符类型

  • ① FString:可以被操作的字符串。开销大于其他类字符串类型

    //FString 字符串类型的三种写法:(必须用 TEXT宏 包裹字符串)
    FString F1 = TEXT("我好想出去玩啊啊啊啊");
    FString F2(TEXT("我好想睡觉啊啊啊啊"));
    FString F3 = FString (TEXT("我不想上班啊啊啊啊"));
    
    //在屏幕上输出F1"我好想出去玩啊啊啊啊"
    GEngine->AddOnScreenDebugMessage(-1, 10, FColor::Red, F1);
    //在日志输出 F1 + F2 + F3(要加*)
    UE_LOG(LogTemp, Log, TEXT("%s\n%s\n%s\n"), *F1, *F2, *F3);

    //① FString 的比较操作(Equals 默认开启大小写)
    if (F1.Equals(F2, ESearchCase::IgnoreCase))
    {
        UE_LOG(LogTemp, Log, TEXT("两个字符串相等"));
    }
    else
    {
        UE_LOG(LogTemp, Log, TEXT("两个字符串不相等咯"));
    }
    
    if (F1 == F2)  //操作符重载==(忽略大小写)
    
    
    //② FString 的查询操作:
    //1.Contains(要查的连续子字符串, 默认忽略大小写, 默认从前往后查),返回bool
    if (F1.Contains(TEXT(""),ESearchCase::IgnoreCase,ESearchDir::FromStart))
    {
        UE_LOG(LogTemp, Log, TEXT("字符串包含“我”"));
    }
    else
    {
        UE_LOG(LogTemp, Log, TEXT("字符串不包含“我”"));
    }
    
    //2.Find(要查的连续子字符串, 默认忽略大小写, 默认从前往后查, 默认查询的起始位置从第一个),返回int32的字符串
    int32 SPlace = F3.Find(TEXT(""),ESearchCase::IgnoreCase,ESearchDir::FromStart,-1);
    //③ FString 的检查操作:
    //IsEmpty()
    if (F2.IsEmpty())
    {
        UE_LOG(LogTemp, Log, TEXT("空字符串"));
    }
    //④ FString 的路径分隔符:/
    F1 = F2 / F3;
    UE_LOG(LogTemp, Log, TEXT("%s"), *F1);
    //运行结果如下:
    //我好想睡觉啊啊啊啊/我不想上班啊啊啊啊
    //⑤ FString 的格式化文本操作:
    FString F4 = FString::Printf(TEXT("%d--%f--%s--"), 232, 3.15f, *F2);
    UE_LOG(LogTemp, Log, TEXT("%s"), *F4);
    //运行结果如下:
    //232--3.150000--我好想睡觉啊啊啊啊--
    //⑥ FString 的裁切操作:
    //Split()
    F1.Split(TEXT(""), &F2, &F3);  //以"玩"为中心截断,前面字符串赋给F2,后面赋给F3
    UE_LOG(LogTemp, Log, TEXT("%s\n%s\n%s\n"), *F2, *F3, *F1);
    //打印结果如下:
    //我好想出去
    //啊啊啊啊
    //我好想出去玩啊啊啊啊
     

     

  • ② FName:资源命名字符串,用来存储名称(散列存储),速度快
    //FName 字符串类型的四种写法:(必须用 TEXT宏 包裹字符串)
    FName N1 = TEXT("张三");
    FName N2(TEXT("张小三"));
    FName N3 = FName(TEXT("张大三"));
    
    //用 FString 初始化 FName
    FString Str = TEXT("张三三");
    FName N4 = *Str;
        
    //在屏幕上输出N1"张三"
    GEngine->AddOnScreenDebugMessage(-1, 10, FColor::Red, N1.ToString());
    
    //在日志输出 N1 + N2 + N3 + N4
    UE_LOG(LogTemp, Log, TEXT("%s\n%s\n%s\n%s"), *N1.ToString(), *N2.ToString(), *N3.ToString(), *N4.ToString());
    
    //① FName 的比较操作(Equals 默认开启大小写)(== 忽略大小写)
    if (N1 == N2)
    {
        UE_LOG(LogTemp, Log, TEXT("两个字符串相等"));
    }
    if (N1.IsEqual(N2))
    {
        UE_LOG(LogTemp, Log, TEXT("两个字符串不相等咯"));
    
    }
    
    
    //② FString 的检查操作:
    //IsNone() 是否为空
    if (N1.IsNone())
    {
        UE_LOG(LogTemp, Log, TEXT("空字符串"));
    }
    
    //IsValid() 是否有效(空字符串也是有效的)
    if (N1.IsValid())
    {
        UE_LOG(LogTemp, Log, TEXT("有效字符串"));
    }
     

     

  • ③ FText:显示字符串,处理用户的显式文本。支持格式化文本,方便构建国际化操作,不提供修改函数,无法进行内容修改
    //六种 FText字符串 的创建方式:
    
    #define LOCTEXT_NAMESPACE "Space"  //声明LOCTEXT宏的本地文本的命名空间
    
    void AMyGameModeBase::BeginPlay()
    {
        Super::BeginPlay();
    
        //1.构建一个空白文本
        FText F1 = FText::GetEmpty();
    
        //2.使用宏构建 NSLOCTEXT(空间名称,键值名称,内容)
        //(只有使用宏构建的FText 才能被文本器收集,罗列到本地的操作面板中)
        FText F2 = NSLOCTEXT("UE5", "k1", "嘿哈嚯");
    
        //3.使用宏构建 LOCTEXT(键值名称,内容)
        //步骤:先声明空间名称 -> 创建对象 -> 卸载宏
        FText F3 = LOCTEXT("k2", "嘿哈嚯嘿嘿");
    
        //4.将 FString 转为 FText
        //(缺点:不能被文本收集器收集)
        FString str(TEXT("嘿哈嚯嘿嘿哈哈哈哈"));
        FText F4 = FText::FromString(str);
    
        //5.将 FName 转为 FText
        //(缺点:也不能被文本收集器收集)
        FName name(TEXT("张三三三"));
        FText F5 = FText::FromName(name);
        //在屏幕上输出F2"嘿哈嚯"
        GEngine->AddOnScreenDebugMessage(-1, 10, FColor::Red, F2.ToString());
    
        //在日志输出F2 + F3 + F4 + F5
        UE_LOG(LogTemp, Log, TEXT("%s\n%s\n%s\n%s\n%s"), *F2.ToString(), *F3.ToString(), *F4.ToString(), *F5.ToString());
        //6.FText 转换
        //① 数字转换:数字int32 转换为 FText
        FText F6_1 = FText::AsNumber(10);
    
        //② 百分比转换:浮点0.3 转换为 FText(30%)
        FText F6_2 = FText::AsPercent(0.3);
    
        //③ 金钱转换:
        FText F6_3 = FText::AsCurrency(666, TEXT("$"));
    
        //④ 日期转换:将秒转换为年月日(单位:秒)
        FText F6_4 = FText::AsDate(66666666666);
    
    
      //7.FRText 的比较操作:比较两个FText是否相同
        //① EqualTo():返回布尔值,不忽略大小写
        if (F2.EqualTo(F3)) {}
    
        //② EqualToCaseIgnored():忽略大小写
        if (F2.EqualToCaseIgnored(F3)) {}
    }
    #undef LOCTEXT_NAMESPACE  //卸载LOCTEXT宏

     

五. 容器

  • 容器是方便存储数据的载体,虚幻提供了三种同质容器(只能用来存储相同类型的数据):
    • TArray(只有TArray可以使用UPROPERTY宏说明)
    • TMap
    • TSet

 

5.1 TArray

  • UE中的数组
  •  速度快,内存消耗小,安全性高
  • TArray 的操作:
    • ① 初始化构建与元素遍历

      void AMyGameModeBase::BeginPlay()
      {
          //1.容器在初始化构建时,直接构建为栈对象,不要new为堆对象!
          TArray<FString> Array;
          
          //2.使用初始化函数添加元素(10个“牛逼”)
          Array.Init(TEXT("牛逼"), 10);
      
          //3.遍历元素: Num()获取TArray当前元素个数
          for (int32 i = 0; i < Array.Num(); i++)
          {
              UE_LOG(LogTemp, Log, TEXT("%s"), *Array[i]);
          }
      
          //3.新语法进行遍历
          for (auto& i : Array)
          {
              UE_LOG(LogTemp, Log, TEXT("NEW: %s"), *i);
          }
      }

       

    • ② 添加元素
      void AMyGameModeBase::BeginPlay()
      {
          //从之前初始化创建的10个元素的数组,添加3条,又接一个长度3的数组,变成16个元素
      
          //4.Add()添加元素到数组末尾,返回当前添加元素在数组中的下标
          int32 Array_A = Array.Add(TEXT("你爹"));
      
          //5.Emplace()添加元素到数组末尾,返回下标
          Array.Emplace(TEXT("你爹1"));
      
          //6.AddUnique()只添加数组中没有的元素,如果数组中已存在该元素,则返回该元素在数组中的下标。
          Array.AddUnique(TEXT("你爹2"));
      
          //7.Append()复制普通数组到TArray容器中
          //ARRAY_COUNT(NewArray)获取新数组长度
          FString NewArray[3]{ TEXT("我是新数组") };
          Array.Append(NewArray, ARRAY_COUNT(NewArray));
      }

 

    • ③ 插入元素
      void AMyGameModeBase::BeginPlay()
      {
          //8.Insert(要插入的元素内容, 插入的数组下标位置)
          //如果插入位置超过容器的大小,将会报错
          //插入后元素依次向后排列,数组长度+1
          Array.Insert(TEXT("插入元素"), 5);
      }

 

    •  ④ 设置容器的大小
      void AMyGameModeBase::BeginPlay()
      {
          //9.SetNum函数,用来设置容器的大小
          TArray<int32>NewArray;
          NewArray.SetNum(20);
      }

       

    • ⑤ 迭代器
      void AMyGameModeBase::BeginPlay()
      {
          //10.迭代器(设计模式):不允许修改元素个数、添加、移除
          //① 创建只读迭代器,只能读取
          for (auto i = Array.CreateConstIterator(); i; ++i)
          {
              UE_LOG(LogTemp, Log, TEXT("%s"), **i);
          }
      
          //② CreateIterator()创建迭代器函数,可以通过迭代器修改元素内容
          for (auto i = Array.CreateIterator(); i; ++i)
          {
              //修改Array元素
              *i = FString(TEXT("改个元素内容"));
              UE_LOG(LogTemp, Log, TEXT("%s"), **i);
          }
      }

 

    • ⑥ 转成普通数组
      void AMyGameModeBase::BeginPlay()
      {
          //11.转成普通数组
          //返回类型指针,指针地址是数组中第一个元素的地址
          FString* NewArray = Array.GetData();
      }

       

    • ⑦ 常规查询函数
      void AMyGameModeBase::BeginPlay()
      {//11.常规查询函数
          //① IsValidIndex():数组在该下标中的元素是否有效,返回布尔值
          Array.IsValidIndex(5);
      
          //② Last():返回容器中最后一个元素
          //   Last(3):返回容器中倒数第三个元素
          //   Top():返回容器中第一个元素
          Array.Last();
          Array.Last(3);
          Array.Top();
      
          //③ Contains():包含,返回布尔值
          Array.Contains(TEXT(""));
      
          //④ Find():查询,返回该元素在数组中的下标(没有则返回-1)
          Array.Find(TEXT("牛逼"));
      
          //查询给定元素所在位置,将位置索引设置到Index,返回布尔值
          //(即能返回布尔,也会返回元素所在位置)
          int Index = 0;
          Array.Find(TEXT("牛逼"), Index);
      }

       

    • ⑧ 常规移除函数
      void AMyGameModeBase::BeginPlay()
      {
          //12.常规移除函数
          //① 在容器中移除给定元素,返回移除的元素个数
          int32 RemoveNum = Array.Remove(TEXT("牛逼"));
      
          //② 移除容器中找到的第一个给定元素
          //(返回1表示移除成功,返回0则说明容器中没有这个元素)
          int32 RemoveIndex = Array.RemoveSingle(TEXT("牛逼"));
      
          //③ 移除给定下标位置的元素
          Array.RemoveAt(2);
      
          //④ 清空容器
          Array.Empty();
      }

       

5.2 TMap

  •  关联型容器,存储对象均有一个关联值,通过键值可以高效的进行对象访问
  • Map的结构例如钥匙和锁,一把钥匙找一把锁,他们之间是对应的关系
  • 映射被销毁时,其元素也将被销毁
  • 键类型必须为值类型,不能使用指针
  • TArray 的操作:
    • ① 初始化构建、添加元素与元素遍历

      void AMyGameModeBase::BeginPlay()
      {
          //1.初始化构建容器,键值类型为FString,数据类型为int32
          TMap<FString, int32>Map;
          
          //2.添加元素
          //注意:添加Map容器中已有的键值,会覆盖之前键值对应的元素
          Map.Add(TEXT("K1"), 111);
      
          //添加元素时,只添加键值会使用默认元素值
          Map.Add(TEXT("K2"));
      
          //可以用这种方法修改数据内容,但不能添加
          Map[TEXT("K2")] = 222;
      
          //3.Append()合并元素
          //将NewMap中的键值和对应元素,复制到Map
          TMap<FString, int32>NewMap;
          Map.Append(NewMap);
      
          //4. 元素遍历
          for(auto& Item : Map)
          {
              Item.Key;  //获取键值
              Item.Value;  //获取元素
              UE_LOG(LogTemp, Log, TEXT("% s,%d"), *Item.Key, Item.Value);
          }
      }

 

    • ② 迭代器
      void AMyGameModeBase::BeginPlay()
      {//5.迭代器
          for (auto i = Map.CreateConstIterator(); i; i++)
          {
              //禁止修改
              i.Key();
              i.Value();
          }
      
          for (auto i = Map.CreateIterator(); i; i++)
          {
              //获得键值
              i.Key();
              i.Value() = 100;
              i->Value;
          }
      }

       

    • ③ 查询函数
      void AMyGameModeBase::BeginPlay()
      {//6.查询函数
          //① 查询容器中元素个数
          Map.Num();
      
          //② 是否包含
          Map.Contains(TEXT("111"));
      
          //③ 返回键值对应的元素指针,如果没找到内容则返回空指针
          Map.Find(TEXT("222"));
      }

       

    • ④ 移除函数
      void AMyGameModeBase::BeginPlay()
      {//7.移除函数
          //① 使用给定键值移除元素
          Map.Remove(TEXT("K1"));
      
          //② 清空容器
          Map.Empty();
      }

       

5.3 TSet

  •  也是键值容器(和TMap类似),但速度快,无需提供单独的键进行关联元素,不允许有重复的键
  • TSet是KV容器,不保证数据填充顺序。

  • TSet数据存储时无法重复存储,TArray可以

  • TSet 的操作:
    • ① 初始化构建、添加元素与元素遍历

      void AMyGameModeBase::BeginPlay()
      {
          //1.初始化构建
          TSet<FString>Set1;
      
          //2.添加元素
          Set1.Add(TEXT("哟西"));
      
          //3.允许合并操作
          TSet<FString>Set2;
          Set1.Add(TEXT("斯国一"));
          Set2.Append(Set1);  //将Set1中元素复制一份添加到Set2中
      
          //4.元素遍历
          for (auto& Item : Set2)
          {
              UE_LOG(LogTemp, Log, TEXT("%s"), *Item);
          }
      }

       

 

    • ② 迭代器
      void AMyGameModeBase::BeginPlay()
      {//5.普通迭代器
          for (auto Item = Set1.CreateIterator(); Item; Item++)
          {
          }
      
          //6.只读迭代器
          for (auto Item = Set1.CreateConstIterator(); Item; Item++)
          {
              *Item;  //获取内容,禁止修改
          }
      }

       

    • ③ 查询函数
      void AMyGameModeBase::BeginPlay()
      {//7.获取容器中元素类型
          Set1.Num();
      
          //8.检查元素中是否包含给定的键值
          Set1.Contains(TEXT("西"));
      
          //9.如果查找某一元素是否存在,可直接使用Find进行单一查找,返回找到指向元素的指针,没找到则返空
          Set1.Find(TEXT("西"));
      
          //10.将TSet容器转为TArray容器
          Set1.Array();
      }

       

    • ④ 移除函数
      void AMyGameModeBase::BeginPlay()
      {//11.移除给定键值内容,成功返回1,失败返回0
          Set1.Remove(TEXT("西"));
      
          //12.移除所有元素,释放空间
          Set1.Empty();
      
          //13.移除元素,但不释放空间
          Set1.Reset();
      }

       

 六. 宏标记

  • 虚幻引擎是模块化编程,先来了解一下继承关系中头文件描述

 

 6.1 UObject

  1. UE中使用的对象大部分继承自UObject(顶级父类)
  2.  UObject具备如下优点:
    • 垃圾回收
    • 引用更新(更新受限于GC回收机制,需使用UPROPERTY宏标记)
    • 序列化(场景中的Actor被保存时发生)
    • 默认属性变化自动更新
    • 源码调整了对象属性时,编译后会自动更新到资源实例上,前提场景实例资源没有修改过属性值,修改过后将使用场景中的修改内容做填充参考
    • 自动属性初始化
    • 自动编辑器整合
    • 运行时类型信息可用
    • 运行时使用Cast可以进行类型信息投射检查(Cast To)
    • 网络复制

3. 创建UObject类对象:

    • NewObject()是最为简单的UObject工厂模式
    • 它需要可选的外部对象和类,并会创建拥有自动生成的名称的新实例

 

 6.2 UClass 宏

  1.  用于标记从UObject派生的类,使得UObject处理系统识别到它们
  2.  语法结构:
    UCLASS(描述指令,描述指令,…)

     

  3. 六个常用宏标记:

    //① 可以作为蓝图中的一种变量类型使用,类默认均可被蓝图访问
    UCLASS(BlueprintType)
    
    //② 默认可被继承,标记关系向子类传递,子类可覆盖描述关系
    UCLASS(Blueprintable)
    UCLASS(BlueprintType,NotBlueprintable)  // 可访问,但不可继承
    
    //③ 将类声明为“抽象基类”,这样会阻止用户在虚幻编辑器中向这个世界中添加这个类的Actor,或者在游戏过程中创建这个类的实例
    UCLASS(Abstract)
    
    //④ 所有属性及函数均为常量,并应作为常量导出。该标识由子类继承
    UCLASS(Const)
    
    //⑤ 类内的成员变量数据信息保存到本地配置文件中,需要显式调用函数SaveConfig使用,并配合UPROPERTY宏操作
    UCLASS(Config=Game) 
    
    //⑥ 用来配置组件在添加时分组情况
    UCLASS(ClassGroup = (TestClass01))

     

 6.3 UFUNCTION 宏

  1. 语法结构:
    UFUNCTION(指令,指令..,meta(key=value))
    
    //注意:在UFUNCTION修饰的函数中,如果参数类型是引用型参数,则在蓝图中将当做返回参数使用,无法查到输入针脚。
    // 如果参数类型是const修饰的引用型参数,则参数被当做输入针脚使用

     

  2. 五个常用宏标记:
    //1. BlueprintCallable:表明此函数可在蓝图中被调用(当类被蓝图继承后才有效果)
    UFUNCTION(BlueprintCallable)
    void Func(int32& A, const int32& B);  //void Func(输出针脚, 输入针脚)
    //如果函数参数是引用类型,则在蓝图中调用被当做输出针脚
    //如果传入参数是const修饰的引用类型,则在蓝图中被当做输入针脚
    
    
    //2. Category:标明此函数在蓝图中的分类
    Category=”UE4Test|MyActor”  // |符号用来划分分类级别
    UFUNCTION(BlueprintCallable,Category=”UE4Test|MyActor”);
    
    
    //3. BlueprintImplementableEvent:用此标记可以在C++中构建函数声明,但是定义由蓝图完成,从而达到C++向蓝图进行调用的目的,在CPP无需定义
    public:
        UFUNCTION(BlueprintImplementableEvent, Category = "UECPP|ACPPActory")
        void Func();
    //必须放在公有或受保护的访问域中,不能在C++中定义函数(本质上相当于纯虚函数virtual)
    //注意:
    //① 无参无返,则在蓝图中当作事件Event使用;
    //② 无参有返,则在蓝图中当作函数使用(需要在函数的overlap中寻找)
    //③ 有参无返(基本数据类型),当作普通事件输入参数使用
    //④ 有参无返(自定义数据类型,如FString),编译不过
    //⑤ 有参无返(基本数据类型/自定义数据类型引用),当作函数看待,在函数表中寻找
    
    
    //4. BlueprintNativeEvent:标记的函数只能在c++中调用,在蓝图中无法调用,此标记可以标注函数可在蓝图中被重写,并且具备在C++中有另一个标记函数
    UFUNCTION(BlueprintNativeEvent)
    void Func01(int32 Num);  //在蓝图中调用
    void Func01_Implementation(int32 Num);  
    //如果蓝图重写此函数,则函数实现在蓝图,如果蓝图没有重写此函数,则函数实现在(函数名_ Implementation)上
    //蓝图中实现后,可以右键函数节点,选择add call to parent function可以调用父类的函数逻辑(类似类中的虚函数,在继承关系中子类可以调用父类的虚函数)
    //注意:
    //① 无参无返,则在蓝图中当作事件Event使用;
    //② 无参有返,则在蓝图中当作函数使用(需要在函数的overlap中寻找)
    
    
    //5.BlueprintPure:特殊标记,构建一个蓝图中的纯函数,用来获取对象数据
    //此标记的函数必须有有效返回值(无返回值编译报错),且在蓝图中无法加入到执行队列,只能以输出值的操作方式被使用,定义实现均放在cpp中
    UFUNCTION(BlueprintCallable, BlueprintPure)

     

 6.4 UPROPERTY 宏

  1. 语法结构:
    UPROPERTY(标记,标记,...,meta(key=value,key=value,...)
    //用于将对象属性暴露到蓝图中操作

     

  2. 常用宏标记:
    • ① Category:分组(定义属性的分类)
      UPROPERTY(Category=CategoryName)
      Type VariableName;
      
      UPROPERTY(Category="CategoryName|SubCategoryName")
      Type VariableName;

       

      UPROPERTY(EditAnywhere, Category = "MyCategory")
      int CategoryNum;
      
      UPROPERTY(EditAnywhere, Category = "MyCategory|SubCategory")
      int SubCategoryNum;

       

    • ② Blueprint
      //----------以下2个不兼容,只能取其一:
      //① BlueprintReadOnly:只读
      UPROPERTY(BlueprintReadOnly)
      
      //② BlueprintReadWrite:可读可写
      UPROPERTY(BlueprintReadWrite)
      
      
      //----------以下2个仅能用于Multicast代理:
      //③ BlueprintAssignable:应显示该属性,以供在蓝图中分配
      UPROPERTY(BlueprintAssignable)
      
      //④ BlueprintCallable:应显示该属性,以在蓝图代码中调用
      UPROPERTY(BlueprintCallable)

       

    • ③ Edit
      //① EditAnywhere:可从编辑器内的属性窗口编辑,在原型和实例中
      //(可以在编辑器窗口中进行编辑,也可在场景细节面板中编辑)
      UPROPERTY(EditAnywhere)
      
      //② EditDefaultsOnly:可通过属性窗口来编辑,仅能对原型编辑
      //(可以在蓝图编辑器中编辑原型数据,但无法在场景细节面板中编辑场景中的具体对象)
      UPROPERTY(EditDefaultsOnly)
      
      //③ EditInstanceOnly:可通过属性窗口来编辑,仅能对实例而非原型进行编辑
      //(属性的修改权限在实例,不能在蓝图编辑器原型中修改)
      UPROPERTY(EditInstanceOnly)
      
      
      //④ EditFixedSize:限定动态数组长度,禁止用户在蓝图属性面板中修改(单一添加无法显式,需要配合使用上面两个标记)
      //只限制修改数组大小,不限制普通变量
      UPROPERTY(BlueprintReadWrite, EditAnyWhere, EditFixedSize)
      
      //⑤ EditInline:使得用户可编辑UnrealEd的属性查看器中的变量所引用的对象属性
      //(仅对对象引用可用,包括对象引用数组)
      UPROPERTY(EditInline)

       

    • ④ Visible
      //① VisibleAnywhere:在属性窗口可见但无法编辑(原型实例中均可看到)
      //如果标记组件指针,则表示组件内容在场景和编辑器的细节面板中显示所有编辑项
      //如果标记的是普通属性(指针、基本数据类型、复合数据类型),则在场景和编辑器中只显示,无法编辑
      UPROPERTY(VisibleAnywhere)
      
      //② VisibleDefaultsOnly:仅在原型蓝图编辑器属性窗口中可见,无法编辑
      //如果标记组件指针,则表示组件内容在场景和编辑器的细节面板中显示所有编辑项
      //如果标记的是普通属性(指针、基本数据类型、复合数据类型),则只在编辑器中会显示(不在场景细节面板中显示),无法编辑
      UPROPERTY(VisibleDefaultsOnly)
      
      //⑩ VisibleInstanceOnly:仅在实例属性窗口中可见,无法编辑
      //如果标记组件指针,则表示组件内容在场景和编辑器的细节面板中显示所有编辑项
      ///如果标记的是普通属性(指针、基本数据类型、复合数据类型),则只在场景细节面板中会显示(不在编辑器中显示),无法编辑
       UPROPERTY(VisibleInstanceOnly) 

       

    • ⑤ 其他
      •  AdvancedDisplay:将属性信息在细节面板中隐藏到高级显式项内容中
        UPROPERTY(EditAnywhere, Category = "Adv")
        int AdvNum1;
        UPROPERTY(EditAnywhere, AdvancedDisplay, Category
        = "Adv") int AdvNum2;

             

         

      • Config:标记此属性可被存储到指定的配置文件中,启动时属性内容将从配置文件中获取
        UPROPERTY(Config)

         

      • meta=(DisplayName=”别名”):别名标记指令
        //给函数或属性取一个别名,用于蓝图显示和搜索
        //可用在修饰属性和函数上,要和暴露到蓝图中的宏连用
        UPROPERTY(BlueprintReadWrite, EditAnyWhere, meta=(DisplayName=”取一个别名”))

         

      • 成员属性值域约束

        //只在UI上约束,填入数据不约束
        UPROPERTY(EditAnyWhere, meta = (UIMin="10", UIMax = "20"))
        
        //UI上约束,填入数据也被约束
        UPROPERTY(EditAnyWhere, meta = (ClampMin = "0", ClampMax = "10"))

         

      • 成员属性修改约束

        //EditCondition="bShow",借助一个布尔变量用来控制另一个变量是否可以在面板中被修改 
        UPROPERTY(EditAnywhere, EditFixedSize) 
        bool bShow; 
        
        UPROPERTY(EditAnywhere, AdvancedDisplay, meta = (ClampMin = "0", ClampMax = "10", EditCondition="bShow")) int32 wock; 
        
        
        //蓝图可以访问C++中private中的属性,作用于在private中不能使用BlueprintReadWrite,加了(AllowPrivateAccess = “true”)之后就会把C++中私有的属性继承到蓝图并变成公有属性
        UPROPERTY(EditAnywhere, EditFixedSize, meta = (AllowPrivateAccess = “true”))

         

七. 结构体与枚举 

 7.1 结构体

  • 由于蓝图特殊,普通的结构体定义无法被蓝图访问,我们需要借助 USTRUCT 宏进行构建UE中的结构体
    USTRUCT(BlueprintType)
    //借助 USTRUCT宏 构建结构体,(BlueprintType:在蓝图中使用)
    
    struct FStructName
    // 结构体名称必须使用F开头
    {
        GENERATED_USTRUCT_BODY();
        //再带一个操作宏
    
        int32 X;
        int32 Y;
    }

     

 7.2 枚举

  •  构建枚举的两种方法
    //(在GameMode.h文件中)
    //第一种方式:空间构建枚举
    UENUM(BlueprintType)
    //宏标记(BlueprintType将类型暴露到蓝图中使用)
    namespace sColor
    {
        enum Type
        {
            Red,
            Blue,
            Yellow,
        };
    }

     

    //第二种方式:构建枚举
    UENUM(BlueprintType)
    enum class EColor :uint8
    {
        Red,
        Blue,
    };
    
    UCLASS(minimalapi)
    class AUECGameMode : public AGameModeBase
    {
        GENERATED_BODY()
    
    public:
    
        //第一种方式构建枚举的两种定义方式:

    //① 定义枚举(只能在C++中使用,不能在蓝图中使用)
    //使用空间名称作为访问依据,可以更清晰的标明意图,方便使用 sColor::Type Etype; //② 定义枚举(既可以在C++中使用,也可以在蓝图) //将变量暴露到蓝图中使用,必须要加 UPROPERTY 相应的宏标记 UPROPERTY(EditAnyWhere,BlueprintReadWrite) TEnumAsByte<sColor::Type> Color;
    
    
        //第二种方式定义枚举:
    //可以暴露到蓝图中使用,但需要对应的宏标记 UPROPERTY(EditAnywhere, BlueprintReadWrite); EColor ColorType;
    
    

     

 7.3 UMETA 宏

  •  可以帮助枚举名进行蓝图别名创建,方便在蓝图中寻找操作(空间声明枚举的方式不适用)
    UENUM(BlueprintType)
    //必须加上
    
    enum ZQState
    {
        Game UMETA(DisplayName = 'ZWJ')
    };

     

八. 资源加载

  • 在虚幻中我们需要处理的资源分为:类资源和非类资源
  • 资源/资产加载到内存中,我们操作需要通过引用来完成,引用分两类:
    • 直接属性引用(硬性引用):直接引用
    • 间接属性引用(软性引用):通过间接机制(如字符串形式的对象路径)来引用
  • 加载方式分为:
    • 同步加载(资源过大会导致游戏程序卡顿)
      • 在加载运行线程中,阻塞线程的流程执行,将线程停止在当前加载逻辑中,加载完成后继续线程的执行逻辑操作
      • 对于加载小资源可以保证资源的不为空,但是加载大资源将导致调用线程卡顿
    • 异步加载
      • 在加载线程中,不阻塞当前线程逻辑加载资源,加载器本身具备线程进行资源加载
      • 比同步加载更加灵活,但维护成本较高,资源加载成功后需要进行回调通知,以完成整个加载流程

 

 8.1 直接属性引用

1. 编辑器直接加载

  • 通过使用属性 UPROPERTY 宏标记,将资产对象指针暴露到编辑器面板,从而直接从编辑器面板拾取资产

2. TSubClassOf

  • 是提供UClass的安全模板类,可以快速构建某种类型对象数据
  • 但只能选取模版类型或继承自模版类型的类 / 蓝图
    //(在.h文件中)

    //1.引用类资源
        //① UClass类:暴露到蓝图中,直接拾取任意类资源(用指针)
        UPROPERTY(EditAnyWhere, BlueprintReadWrite)
        UClass* CharacterClass;

     

        //② TSubclassOf:只能用来拾取 AMyActor 类或其子类(用<>包裹)(记得加头文件)
        UPROPERTY(EditAnyWhere, BlueprintReadWrite)
        TSubclassOf<AMyActor> MyActor;
        MyActor.GetClass();  //获取UClass指针

    //
    2.引用非类资源
        UPROPERTY(EditAnyWhere, BlueprintReadWrite)
        USoundBase* Sound;  //拾取音频资源
    //拾取音频还可以用:USoundCue(加头文件#include<Sound/USoundCue.h>)
    UPROPERTY(EditAnyWhere, BlueprintReadWrite) UStaticMesh* Mesh; //拾取静态网格体 UPROPERTY(EditAnyWhere, BlueprintReadWrite) UStaticMeshComponent* Mesh; //静态网格体组件(装载资产的组件) UPROPERTY(EditAnyWhere, BlueprintReadWrite) USkeletalMesh* SMesh; //拾取骨骼网格体
    
    
        //3.加载类资源和非类资源
        //UObject类
        UPROPERTY(EditAnyWhere, BlueprintReadWrite)
        UObject* Object;
     

      

3. 构造函数加载

  • 借助构造函数资产加载类进行资源引用
  • ConstructorHelpers可以进行类引用,源资源引用
  • ConstructorHelpers 只能在构造函数中使用,GameInstance中是 Init 函数(需要重载)
  • 常用的资源加载分类:
    • FClassFinder:加载创建后的蓝图对象

      ConstructorHelpers::FClassFinder<APawn>(TEXT("/Game/Flappybird/Blueprints/BP_Bird")).Class;//返回数据类型是TSubClassof
      
      //使用路径拾取蓝图对象(要加_C)
      ConstructorHelpers::FClassFinder<AActor> UnitSelector(TEXT(“Blueprint‘/Game/Blueprints/MyBlueprint.MyBlueprint_C’”));//下划线C必须要加

       

    • FObjectFinder:加载各种资源(如音频,图片,材质,静态网格)

      ConstructorHelpers::FObjectFinder<UTexture2D> BarFillObj(TEXT("/Game/UI/HUD/BarFill"));
      BarFillTexture = BarFillObj.Object;  //将获取的数据内容指针保存

       

    • 注意:
      • 操作路径前加入 /Game/ 前缀
      • 加载蓝图类模版对象时,需要加注“_C”
      • 加载失败或是未找到资源,对象内的资产属性为null

 4. 查找加载

  • 在只知道目标资源路径的基础上,进行运行时态的资源加载
  • 资源加载可能会失败或是无效,所以需要对操作的结果进行判定
    • LoadClass:加载类资产,可放在任何地方加装 , 返回UClass指针,可用TSubclassOf来接
      TSubclassOf<AMyActor> Player1 = LoadClass<AMyActor>(NULL, TEXT("Blueprint'/Game/BP/BP_Actor.BP_Actor_C'"));

       

    • LoadObject:加载类资源 + 非类资产,返回该类的指针
      AMyActor* Player2 = LoadObject<AMyActor>(NULL, TEXT("Blueprint'/Game/BP/BP_Actor.BP_Actor_C'"));

       

 5. 路径引用 

  • 此处的 “文件路径” 是复制绝对路径

 

 

 8.2 间接属性引用

 1. FSoftObjectPath

  • 是一个简单的结构体,使用一个字符串包含资源的完整名称
  • 可以在编辑器中拾取资源(与直接属性引用相同),但没有把资源加载到内存(资源的加载需要通过额外的代码编写完成)
  • 对于资源的拾取并没有特定的要求,所有能够被序列化的资源均能被拾取(类资源,非类资源)

 2. FSoftClassPath

  • 仅拾取类资源链接
    //(在.h文件中)
    //间接属性引用
    //1.FSoftClassPath,用于拾取类资源的链接
        UPROPERTY(EditAnywhere)
        FSoftClassPath CharacterPath;
        //2.FSoftObjectPath:(类资源 + 非类资源)暴露到蓝图中拾取的是一个链接,不会进行资源加载
        UPROPERTY(EditAnyWhere)
        FSoftObjectPath SoundPath;
     

 

 8.3 同步加载和异步加载

  • 同步加载(加载大资源将导致调用线程卡顿)
  • 异步加载(资源加载成功后需要进行回调通知)
    • FSreamableManager 构建异步处理逻辑,创建在全局游戏的单例对象中,结合FSoftObjectPath进行加载
      //(在UECCharacter.h文件中)
      
      #include <Engine/StreamableManager.h>  //资源加载器头文件引入
      UCLASS(config=Game) class AUECCharacter : public ACharacter { public:
          //1.同步加载
          //加载播放音效资源
          void LoadPlaySound();
      
      
          //2.异步加载
          //① 声明资源加载器(构建为栈对象,需引入头文件,不要构建为堆对象)
          FStreamableManager LoadStream;
      
          //② 回调通知函数
          void LoadCallBack();
      };


      //
      (在UECCharacter.cpp文件中) #include <Kismet/GameplayStatics.h> //播放音频头文件引入 void AUECCharacter::LoadPlaySound() { if (!SoundPath.IsValid()) //判断资产链接是否有效 { //如果资产链接无效,则直接设置资产路径(可以用来替代细节面板中的引用) SoundPath.SetPath(TEXT("SoundCue'/Game/StarterContent/Audio/Collapse_Cue.Collapse_Cue'")); }
      //尝试加载资源 UObject*Source = SoundPath.TryLoad();
          //1.同步加载(会阻塞线程)
          UObject* Source = LoadStream.LoadSynchronous(SoundPath);
      
      
          //2.异步加载
          //① 请求加载,并绑定回调函数(资源,单播)
          LoadStream.RequestAsyncLoad(SoundPath,FStreamableDelegate::CreateUObject(this,&AUECCharacter::LoadCallBack));
      
      }
      //② 回调函数调用(表明异步加载完成)
      void AUECCharacter::LoadCallBack()
      {
          //类型转换Cast
          //SoundPath.ResolveObject():获取异步加载好的资源引用
          USoundBase* Sound = Cast<USoundBase>(SoundPath.ResolveObject());
      
          if (Sound)
          {
              //播放音效
              UGameplayStatics::PlaySound2D(GetWorld(), Sound);
          }
      }
      
      

       

 8.4 模板资源拾取类

 1. TSoftObjectPtr和TSoftClassPtr

  •  TSoftObjectPtr和TSoftClassPtr也分为同步加载与异步加载
    UPROPERTY(EditAnywhere)
    TSoftObjectPtr<class UStaticMesh> SoftMesh;
    UPROPERTY(EditAnywhere)
    TSoftClassPtr<class ATestActor> SoftTestActor;

     

  • TSoftObjectPtr
    • 同步加载
      //(.h 文件)
      
      UPROPERTY(EditAnywhere)
      TSoftObjectPtr<class UStaticMesh> SoftMesh;
      // 构建为栈对象,需要引入头文件,不要构建为堆对象
      // #include "Engine/StreamableManager.h"
      FStreamableManager m_Streamable;
      //(.cpp文件)
      
      // 可以转换为FSoftObjectPath对象
      SoftMesh.ToSoftObjectPath();
      
      // 同步加载
      UObject* Source = m_Streamable.LoadSynchronous(SoftMesh);
      UStaticMesh* Mesh = Cast<UStaticMesh>(Source);
      if (Mesh) 
      {
          UKismetSystemLibrary::PrintString(this, TEXT("加载成功!"));
      }

       

    • 异步加载
      //(.h文件)
      
      // 异步加载回调函数
      void LoadSourceCallback();
      //(.cpp文件)
      
      // 异步加载
      //需要在初始化函数中绑定回调函数,然后实现回调函数
          m_Streamable.RequestAsyncLoad(SoftMesh.ToSoftObjectPath(), FStreamableDelegate::CreateUObject(this, &UTestGameInstance::LoadSourceCallback));

       

  • TSoftClassPtr
    • 同步加载
      //(.h文件)
          
      UPROPERTY(EditAnywhere)
      TSoftObjectPtr<class UStaticMesh> SoftMesh;
      // 构建为栈对象,需要引入头文件,不要构建为堆对象
      // #include "Engine/StreamableManager.h"
      FStreamableManager m_Streamable;
      //(.cpp文件)
      
      // 同步加载
      TSubclassOf<ATestActor> TestActorClass = m_Streamable.LoadSynchronous(SoftTestActor);
      if (TestActorClass) 
      {
          GetWorld()->SpawnActor<ATestActor>(TestActorClass);
      }

       

    • 异步加载
      //(.cpp文件)
      
      // 异步加载
      m_Streamable.RequestAsyncLoad(SoftTestActor.ToSoftObjectPath(), FStreamableDelegate::CreateUObject(this, &UTestGameInstance::LoadSourceCallback));
      
      //回调函数实现
      void UTestGameInstance::LoadSourceCallback() {
      // 此函数调用,则表明异步加载完成
      UClass* TestActorClass = SoftTestActor.Get();
          if (TestActorClass) 
      { GetWorld()
      ->SpawnActor<ATestActor>(TestActorClass); } }

       

 

 

九. 代理

  • 代理可以解决一对一或一对多的任务分配工作,还可以解决通知问题
  • 通过代理完成调用某一个对象的一个函数,而不直接持有该对象的任何指针
  • (要调用某个函数,但并不直接去调用,而是通过另一个入口去调用,这就是代理)
  • 代理分为:
    • 单播代理(只能进行通知一个人)
    • 多播代理(可以进行多人通知)
    • 动态代理(可以被序列化,体现在可与蓝图进行交互,C++中可以将通知事件进行蓝图广播)

 

 9.1 单播代理

  • 单播代理只能绑定一个通知对象,无法进行多个对象通知
  • 通过宏进行构建,分为两种:
    • 有返回类型
    • 没有返回类型
  • 常用绑定函数:
    • BindUObject:绑定UObject类型对象成员函数的代理
    • BindSP:绑定基于共享引用的成员函数代理
    • BindRaw:绑定原始自定义对象成员函数的代理,操作调用需要注意执行需要检查IsBound 
    • BindStatic:绑定全局函数成为代理
    • UnBind:解除绑定代理关系
    • (绑定中传递的对象类型必须和函数指针所属类的类型相同,否则绑定会报错)
  • 调用执行:
    • 执行Execute函数之前,需要检查是否存在有效绑定使用函数 IsBound
    • Execute 调用代理通知,不安全
    • ExecuteIfBound 调用代理通知,安全,但是有返回类型的回调函数无法使用此函数执行回调
    • IsBound 检查当前是否存在有效代理绑定
  • 构建步骤:
    • 通过宏进行声明代理对象类型(根据回调函数选择不同的宏)

    • 使用代理类型进行构建代理对象

    • 绑定回调对象、操作函数

    • 执行代理对象回调

      //(在.h文件中)
      //1.构建单播代理(此例以无参无返单播代理为例)
      //① 单播代理:无参数无返回值单播代理
      DECLARE_DELEGATE(CallTest)
      //② 1个参数的单播代理 DECLARE_DELEGATE_OneParam(Delegate_OneP, int32)
      //③ 2个参数的单播代理 DECLARE_DELEGATE_TwoParams(Delegate_TwoP, int32, int32)
      //④ 有返回值,和2个参数的单播代理 DECLARE_DELEGATE_RetVal_TwoParams(float, Delegate_RTwoP, float, float)
      
      UCLASS(config=Game)
      class AUECCharacter : public ACharacter
      {
          GENERATED_BODY()
      public:
          //2. 声明无参无返的单播代理
          CallTest Dnop;
      
          //3.创建要绑定到无参无返的单播代理上的函数
          void Func();
          virtual void BeginPlay();
      };

       

      //(在.cpp文件中)

      void AUECCharacter::Func()
      {
          UE_LOG(LogTemp, Log, TEXT("---------我是无参无返的单播代理!---------"));
      }
      
      
      void AUECCharacter::BeginPlay()
      {
          Super::BeginPlay();
      
          //4.将Func函数绑定到单播代理
          Dnop.BindUObject(this, &AUECCharacter::Func);
      
          //5.判断单播代理是否绑定
          if (Dnop.IsBound())
          {
              //6.调用代理,执行绑定的函数
              Dnop.Execute();
          }
      }
       

       

       

 9.2 多播代理

  • 无法构建具有返回值的多播代理,只能构建参数
  • 广播

    • 调用函数Broadcast,但是调用不保证执行顺序的正确性
  • 构建步骤:
    • 使用宏构建代理类型

    • 使用代理类型构建多播代理对象

    • 添加绑定代理

    • 执行调用

      //(在.h文件中)
      //1.构建多播代理(不可以构建有返回值的)
      //① 无参的多播代理
      DECLARE_MULTICAST_DELEGATE(Delegate_MultiNoP)
      
      //② 单播代理:有2个参数的单播代理
      DECLARE_MULTICAST_DELEGATE_TwoParams(Delegate_MultiTwoP, int32, int32)
      
      UCLASS(config=Game)
      class AUECCharacter : public ACharacter
      {
          GENERATED_BODY()
      public:
          //2. 声明多播代理
          Delegate_MultiNoP MultiNoP;
          Delegate_MultiTwoP MultiTwoP;
      
          //3.创建要绑定到无参无返的单播代理上的函数
          void Func1_MultiNoP();
          void Func2_MultiNoP();
      
          void Func1_MultiTwoP(int32 A, int32 B);
          void Func2_MultiTwoP(int32 C, int32 D);
      
          virtual void BeginPlay();
      
      
      };

       

      //(在.cpp文件中)
      void AUECCharacter::Func1_MultiNoP()
      {
          UE_LOG(LogTemp, Log, TEXT("---------我是无参的多播代理Func1_MultiNoP()!---------"));
      }
      void AUECCharacter::Func2_MultiNoP()
      {
          UE_LOG(LogTemp, Log, TEXT("---------我是无参的多播代理Func2_MultiNoP()!---------"));
      }
      void AUECCharacter::Func1_MultiTwoP(int32 A, int32 B)
      {
          UE_LOG(LogTemp, Log, TEXT("---------我是有2个int参AB的多播代理Func1_MultiTwoP!---------"));
      }
      void AUECCharacter::Func2_MultiTwoP(int32 C, int32 D)
      {
          UE_LOG(LogTemp, Log, TEXT("---------我是有2个int参CD的多播代理Func1_MultiTwoP!---------"));
      }

       

      void AUECCharacter::BeginPlay()
      {
          Super::BeginPlay();
      
          //4.将多个函数绑定到多播代理
          MultiNoP.AddUObject(this, &AUECCharacter::Func1_MultiNoP);
          MultiNoP.AddUObject(this, &AUECCharacter::Func2_MultiNoP);
      
          MultiTwoP.AddUObject(this, &AUECCharacter::Func1_MultiTwoP);
          MultiTwoP.AddUObject(this, &AUECCharacter::Func2_MultiTwoP);
      
          //5.判断多播代理是否绑定
          if (MultiNoP.IsBound())
          {
              //6.调用多播代理,执行绑定的函数
              MultiNoP.Broadcast();
          }
      
          //5.判断多播代理是否绑定
          if (MultiTwoP.IsBound())
          {
              //6.调用多播代理,执行绑定的函数
              MultiTwoP.Broadcast(2,3);
          }
          //清空所有绑定函数
          MultiNoP.Clear();
      
          //移除单绑定,需要传递 FDelegateHandle(代理句柄)
          FDelegateHandle DH_Fun = MultiTwoP.AddUObject(this, &AUECCharacter::Func1_MultiTwoP);
          MultiTwoP.Remove(DH_Fun);
      
          //移除某个对象中的所有函数绑定
          MultiTwoP.RemoveAll(this);
      
      
      }
       

       

 

 9.3 动态代理

  •  是允许被序列化的数据结构(代理可以被数据化提供给蓝图进行使用),使得可以在.cpp中调用代理广播,并将事件通知到蓝图
  •  UE中的大部分通知事件均使用动态代理,方便蓝图操作(如碰撞通知)
  •  动态代理无法使用带有返回值的函数进行构建
  •  动态代理分为
    • 动态单播(但是创建动态单播还不如创建普通的多播,单播无法在蓝图中绑定,无法使用宏BlueprintAssignable修饰)
    • 动态多播
  • 动态代理与上面单播多播的区别:
    • 动态代理的类型名称需要用 F 开头(动态代理实现了机制构建类)
    • 动态代理对象类型可以使用UPROPERTY标记;其他代理均无法使用(不加编译可过,调用就会出错)
    • 动态代理绑定对象的函数需要使用UFUNCTION进行描述(因为需要跟随代理被序列化)
  • 构建步骤
    • 通过宏进行声明代理对象类型(根据回调函数选择不同的宏)

    • 使用代理类型进行构建代理对象

    • 绑定回调对象、操作函数

    • 执行代理对象回调

 

 

 

 

 

 

十. 事件与接口

 

 

 

 

 

 

 

 

 

 

 

 

 

十一. 智能指针

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

十二. 协同开发

  • 不可避免地,在学习工作中会遇到多人开发的场景

 

  •  什么是SVN?
    • 免费的代码版本管理工具,版本控制(版本回退),功能一般
    • 操作简单(但容易产生奇奇怪怪的问题),适合新手入门,易于搭建局域网服务器
  • 其他的版本控制工具
    • Git
      • 免费且功能强大,可以和 github 无缝衔接
      • 概念和操作繁多,学习有一定难度
    • Perforce
      • 中心化管理(客户端从服务器下载、更新、修改、上传),易学易用
      • 收费,适合中大型团队(小团队免费)

 

 

12.1 SVN服务器搭建和使用

1. 下载地址: http://subversion.apache.org/packages.html

 

2. 建议:VisualSVN server 服务端和 TortoiseSVN客户端搭配使用

3. 下载VisualSVN server,下载完成后双击安装:

 

 

 

 

 

 

 

 4. 创建版本库:启动VisualSVN Server Manager,可以在窗口的右边看到版本库的一些信息(状态,日志,用户认证,版本库之类的)

 

 

 

 

 

 

 

5. 建立用户:需要建立用户和组,并且需要分配权限

          

 

        

 

 6. 设置权限:

          

 

 12.2 SVN客户端安装

 1. 下载 ”svn小乌龟”后,进行安装:

 

 

2. 无脑安装:

 

 

3. checkout项目文件:

 

 

 

 4. 获取URL:URL可以在SVN服务器获取到

 

 

 

 

5. Checkout 检出:

 

 

 

 

 12.3 协同开发的常遇问题

  1.  

 

 

 


 

附:一些杂七杂八的东西

 UE 引擎相关操作

彻底删除无用的UEC文件

  •  在编译器源码中删除相关文件,再删除工程文件夹对应浏览器目录下的相关文件,最后返回编译器中编译项目
  •  反之,如果没删干净,下次再创建同名 C++ 类时就会出现奇奇怪怪的报错

 

修改引擎编辑器

 

VS 编辑器相关操作

VS 中的一些快捷键

  • F12:跳转查看源码中函数定义
  • Ctrl + F :查找
  • Ctrl + 点击函数:跳转查看源码中函数定义

 

使用插件 Visual Assist X 小番茄的常用快捷键

  • Alt + O:在源文件 / 头文件间切换
  • Alt + G:在声明 / 定义间切换
  • Alt + Shift + O:在整个解决方案中查找文件
  • Alt + Shift + S:在整个解决方案中查找 Symbol(类、结构体、枚举、方法、变量)

 

修改 VS 编译选项框

  • 当编译选项框过小  
  • 可以在工具栏任意处右键,自定义 - 命令:工具栏:标准(选择“解决方案配置”)- 修改所选内容

     

  • 效果如下:

 

 解决编译器中文乱码

  • 如果出现中文乱码的情况:高级保存选项 -> 将编码设置为UTF-8格式

 

 

  •  但是可能有些朋友会发现在 “文件” 的地方找不到 “高级保存选项” 这玩意,可能是因为你没有打开:

 

 

 

 

 

  1. 创建静态网格体组件,并设置为根组件
    // 在.cpp文件:
    #include "Components/StaticMeshComponent.h"  //静态网格体组件头文件
    
    // 构造函数中初始化
    AMyActor::AMyActor()
    {
        // 创建一个静态网格体组件,并设置为根组件
        RootComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("组件别名"));
    }

     

 

 

 

 

 

 

 

 

 

 

 

 

未完待续...

posted @ 2022-11-15 09:37  哟吼--小文文公主  阅读(8760)  评论(3)    收藏  举报