[{"content":"一、SSH 是什么 SSH（Secure Shell）是一种加密网络协议。在 Git 场景下，SSH 密钥对用来代替密码向 GitHub 证明身份。\n核心原理：\n私钥留在本地电脑，永远不要分享 公钥上传到 GitHub 推送/拉取代码时，本地用私钥签名，GitHub 用公钥验证 二、配置流程（共 6 步） 步骤 1：检查是否已有 SSH 密钥 1 ls -al ~/.ssh/ 命令拆解：\nls：list，列出目录下的文件 -a：显示所有文件（包括 . 开头的隐藏文件） -l：以长格式显示（权限、大小、日期等） ~：用户主目录，Windows 上即 C:\\Users\\用户名 .ssh/：SSH 配置目录（隐藏文件夹） 如果看到 id_ed25519 和 id_ed25519.pub，说明已有密钥，可跳过步骤 2。 如果只有 known_hosts 或目录为空，说明没有密钥，继续下一步。\n步骤 2：生成新的 SSH 密钥 1 ssh-keygen -t ed25519 -C \u0026#34;你的GitHub邮箱\u0026#34; 命令拆解：\nssh-keygen：SSH 密钥生成工具 -t ed25519：指定算法为 Ed25519（比 RSA 更短更安全更快，目前最推荐） -C \u0026quot;邮箱\u0026quot;：注释，方便日后识别密钥归属 执行后两个交互提示：\n保存位置 → 直接回车，使用默认路径 ~/.ssh/id_ed25519 passphrase（口令） → 可留空回车，也可设密码保护私钥 生成结果：.ssh/ 下出现两个文件：\nid_ed25519 → 私钥（不要分享） id_ed25519.pub → 公钥（上传到 GitHub） 步骤 3：启动 ssh-agent 并添加私钥 1 2 3 4 5 # 启动 ssh-agent（后台密钥管理器） eval \u0026#34;$(ssh-agent -s)\u0026#34; # 将私钥添加到 agent ssh-add ~/.ssh/id_ed25519 命令拆解：\nssh-agent：后台运行的密钥管理器，管理你的私钥，避免每次 git 操作都重复认证 -s：输出适合 shell 使用的格式 eval \u0026quot;$(…)\u0026quot;：将 ssh-agent 输出的环境变量在当前 shell 中生效 ssh-add：将私钥交给 agent 管理 成功标志：\nAgent pid XXXX（agent 已启动） Identity added: /c/Users/…/.ssh/id_ed25519（私钥已加载） 步骤 4：复制公钥 1 cat ~/.ssh/id_ed25519.pub 命令拆解：\ncat：concatenate，将文件内容输出到屏幕 会输出一行以 ssh-ed25519 开头的字符串，完整复制这一行，下一步要用。\n⚠️ 注意复制的是 .pub 公钥文件，不要复制私钥！\n步骤 5：将公钥添加到 GitHub 浏览器操作：\n打开 https://github.com/settings/keys 点击 New SSH key 填写： Title：随便起名，如 My-Laptop（标识哪台电脑） Key type：保持默认 Authentication Key Key：粘贴步骤 4 复制的公钥 点击 Add SSH key，GitHub 可能要求确认密码 步骤 6：测试 SSH 连接 1 ssh -T git@github.com 命令拆解：\nssh：SSH 客户端 -T：不分配终端（只测试认证） git@github.com：以 git 用户身份连接 GitHub 第一次连接会提示 Are you sure you want to continue connecting?，输入 yes 回车。\n成功标志：\n1 Hi Moqi-ui! You\u0026#39;ve successfully authenticated, but GitHub does not provide shell access. 三、配置完成后如何使用 克隆仓库 1 2 3 4 5 # SSH 方式（现在可用） git clone git@github.com:Moqi-ui/仓库名.git # HTTPS 方式（之前的做法） git clone https://github.com/Moqi-ui/仓库名.git 已有仓库从 HTTPS 切换到 SSH 1 2 3 4 5 # 查看当前远程地址 git remote -v # 切换为 SSH 地址 git remote set-url origin git@github.com:Moqi-ui/仓库名.git 四、常见问题 Q1：一个密钥可以访问多个仓库吗？ 可以。 SSH 密钥绑定的是整个 GitHub 账户，不是单个仓库。添加公钥后，该账户下所有仓库都可以通过 SSH 访问。\nQ2：多台电脑怎么办？ 每台电脑各自生成一对密钥，将各自的公钥都添加到 GitHub 即可（GitHub 支持添加多个 SSH key）。\nQ3：SSH 和 GPG 有什么区别？ 维度 SSH 密钥 GPG 密钥 用途 身份认证（push/pull/clone） 给 commit 签名 效果 替代 HTTPS 密码 commit 旁显示绿色 \u0026ldquo;Verified\u0026rdquo; 标签 是否必须 用 SSH 协议时必须 完全可选 Q4：ssh-agent 每次打开 Git Bash 都要重新启动吗？ 是的。eval \u0026quot;$(ssh-agent -s)\u0026quot; 只在当前 shell 会话生效。关闭 Git Bash 后 agent 就停止了。如果不希望每次手动执行，可以在 ~/.bashrc 或 ~/.bash_profile 中添加自动启动的配置。\nQ5：passphrase 忘了怎么办？ 无法找回。只能重新生成密钥，然后更新 GitHub 上的公钥。\nQ6：SourceTree 提示 SSH 密钥认证失败？ 原因： SourceTree 默认使用 PuTTY/Plink 作为 SSH 客户端，而我们生成的密钥是 OpenSSH 格式，两者不兼容。\n解决方法： 切换 SourceTree 的 SSH 客户端为 OpenSSH，无需重新生成密钥。\n操作步骤：\n打开 SourceTree → 菜单栏 工具 → 选项 选择 一般 选项卡 找到 SSH 客户端配置 区域 将 SSH 客户端从 PuTTY/Plink 改为 OpenSSH 点击 确定 💡 补充说明：\nPuTTY/Plink：使用 .ppk 格式的密钥（PuTTY 专用格式），密钥由 PuTTYgen 生成，由 Pageant 管理 OpenSSH：使用 id_ed25519 格式的密钥（标准 SSH 格式），密钥由 ssh-keygen 生成，由 ssh-agent 管理 我们在步骤 2 中用 ssh-keygen 生成的是 OpenSSH 格式密钥，所以 SourceTree 也需要切换为 OpenSSH 客户端 五、命令速查表 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 # 检查密钥 ls -al ~/.ssh/ # 生成密钥 ssh-keygen -t ed25519 -C \u0026#34;邮箱\u0026#34; # 启动 agent eval \u0026#34;$(ssh-agent -s)\u0026#34; # 添加私钥 ssh-add ~/.ssh/id_ed25519 # 查看公钥 cat ~/.ssh/id_ed25519.pub # 测试连接 ssh -T git@github.com # 切换远程地址为 SSH git remote set-url origin git@github.com:用户名/仓库名.git ","permalink":"https://imrcao.top/posts/0.7github%E4%B8%8A%E7%9A%84ssh%E5%AF%86%E9%92%A5%E9%85%8D%E7%BD%AE/","summary":"一、SSH 是什么 SSH（Secure Shell）是一种加密网络协议。在 Git 场景下，SSH 密钥对用来代替密码向 GitHub 证明身份。\n核心原理：\n私钥留在本地电脑，永远不要分享 公钥上传到 GitHub 推送/拉取代码时，本地用私钥签名，GitHub 用公钥验证 二、配置流程（共 6 步） 步骤 1：检查是否已有 SSH 密钥 1 ls -al ~/.ssh/ 命令拆解：\nls：list，列出目录下的文件 -a：显示所有文件（包括 . 开头的隐藏文件） -l：以长格式显示（权限、大小、日期等） ~：用户主目录，Windows 上即 C:\\Users\\用户名 .ssh/：SSH 配置目录（隐藏文件夹） 如果看到 id_ed25519 和 id_ed25519.pub，说明已有密钥，可跳过步骤 2。 如果只有 known_hosts 或目录为空，说明没有密钥，继续下一步。\n步骤 2：生成新的 SSH 密钥 1 ssh-keygen -t ed25519 -C \u0026#34;你的GitHub邮箱\u0026#34; 命令拆解：\nssh-keygen：SSH 密钥生成工具 -t ed25519：指定算法为 Ed25519（比 RSA 更短更安全更快，目前最推荐） -C \u0026quot;邮箱\u0026quot;：注释，方便日后识别密钥归属 执行后两个交互提示：\n保存位置 → 直接回车，使用默认路径 ~/.ssh/id_ed25519 passphrase（口令） → 可留空回车，也可设密码保护私钥 生成结果：.","title":"Github中SSH密钥配置流程"},{"content":"前言： 最近在学习UE4网络相关知识，尝试学习使用 UDP做一个分布式服务器（后面会出相关文章），这里简单记录一下如何使用 UE4的“Sockets”模块和“Networking”模块进行UDP广播。\n操作步骤： 1、首先创建两个空的UE4C++工程（我这里使用的是UE426版本引擎），一个用来广播数据，一个用来接收数据，在两个工程分别创建继承自UObject的类，分别命名为“ UDPSend”和“ UDPRecive”，还需在\u0026quot;.build.cs\u0026quot;文件中添加模块“Sockets\u0026quot;, \u0026ldquo;Networking”，示例如下代码所示。 2、为了清晰可见，直接上代码（部分代码是网上粘贴的，我这里只是想记录的同时再复习一遍，也方便自己后续查阅）。 3、编译后启动引擎，将两个基于C++的蓝图类，分别拖入到场景中（用来广播的工程只用创建UDPSend，用来接收的工程只用创建“UDPRecive”）。拖入场景中后，在UDPSend中调用函数“StartUDPSender()”，传入网络套接字、 IP、端口号即可。UDP开启后，可以TICK执行广播数据（调用函数RamaUDPSender_SendString（））；接收数据的工程也是一样，将对应类拖入场景中，打开蓝图编辑器，先调用函数StartUDPReceiver（），再调用函数DataRecv（）。两个工程要同时运行，这样就能广播和接收数据了。 using UnrealBuildTool;\npublic class UDPTest426 : ModuleRules { public UDPTest426(ReadOnlyTargetRules Target) : base(Target) { PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 PublicDependencyModuleNames.AddRange(new string[] { \u0026#34;Core\u0026#34;, \u0026#34;CoreUObject\u0026#34;, \u0026#34;Engine\u0026#34;, \u0026#34;InputCore\u0026#34;, \u0026#34;Sockets\u0026#34;, \u0026#34;Networking\u0026#34; }); PrivateDependencyModuleNames.AddRange(new string[] { }); // Uncomment if you are using Slate UI // PrivateDependencyModuleNames.AddRange(new string[] { \u0026#34;Slate\u0026#34;, \u0026#34;SlateCore\u0026#34; }); // Uncomment if you are using online features // PrivateDependencyModuleNames.Add(\u0026#34;OnlineSubsystem\u0026#34;); // To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true } } //UDPSend.h #pragma once #include \u0026#34;CoreMinimal.h\u0026#34; #include \u0026#34;GameFramework/Actor.h\u0026#34; #include \u0026#34;Runtime/Networking/Public/Networking.h\u0026#34; //#include \u0026lt;iostream\u0026gt; //#include \u0026lt;list\u0026gt; //#include \u0026lt;numeric\u0026gt; //#include \u0026lt;algorithm\u0026gt; #include \u0026#34;UDPSend.generated.h\u0026#34; UCLASS() class UDPTEST426_API AUDPSend : public AActor { GENERATED_BODY() public: // Sets default values for this actor\u0026#39;s properties AUDPSend(); protected: // Called when the game starts or when spawned virtual void BeginPlay() override; public: // Called every frame virtual void Tick(float DeltaTime) override; //新建isudp的bool变量，表示用UDP进行通信 bool IsUDP; //新建函数RamaUDPSender_SendString(),用于发送消息 UFUNCTION(BlueprintCallable, Category = \u0026#34;UDP\u0026#34;) bool RamaUDPSender_SendString(FString ToSend); public: TSharedPtr\u0026lt;FInternetAddr\u0026gt; RemoteAddr; FSocket* SenderSocket; // SocketName,IP,Port,IsUdp UFUNCTION(BlueprintCallable, Category = \u0026#34;UDP\u0026#34;) bool StartUDPSender(const FString\u0026amp; YourChosenSocketName, const FString\u0026amp; TheIP, const int32 ThePort, bool UDP); //此函数用于在虚幻C++内获取本地IP，转化为String类型 UFUNCTION(BlueprintPure, Category = \u0026#34;UDP\u0026#34;) FString GetIP(); UFUNCTION(BlueprintCallable, Category = \u0026#34;UDP\u0026#34;) bool SendGameTime(FString ToSend); public: UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = \u0026#34;UDP\u0026#34;) bool ShowOnScreenDebugMessages; FORCEINLINE void ScreenMsg(const FString\u0026amp; Msg) { if (!ShowOnScreenDebugMessages) return; GEngine-\u0026gt;AddOnScreenDebugMessage(-1, 5.f, FColor::Red, *Msg); } FORCEINLINE void ScreenMsg(const FString\u0026amp; Msg, const float Value) { if (!ShowOnScreenDebugMessages) return; GEngine-\u0026gt;AddOnScreenDebugMessage(-1, 5.f, FColor::Red, FString::Printf(TEXT(\u0026#34;%s %f\u0026#34;), *Msg, Value)); } FORCEINLINE void ScreenMsg(const FString\u0026amp; Msg, const FString\u0026amp; Msg2) { if (!ShowOnScreenDebugMessages) return; GEngine-\u0026gt;AddOnScreenDebugMessage(-1, 5.f, FColor::Red, FString::Printf(TEXT(\u0026#34;%s %s\u0026#34;), *Msg, *Msg2)); } public: virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; }; //UDPSend.cpp #include \u0026#34;UDPSend.h\u0026#34; AUDPSend::AUDPSend() { // Set this actor to call Tick() every frame. You can turn this off to improve performance if you don\u0026#39;t need it. PrimaryActorTick.bCanEverTick = true; SenderSocket = NULL; ShowOnScreenDebugMessages = true; } // Called when the game starts or when spawned void AUDPSend::BeginPlay() { Super::BeginPlay(); } // Called every frame void AUDPSend::Tick(float DeltaTime) { Super::Tick(DeltaTime); } bool AUDPSend::StartUDPSender(const FString\u0026amp; YourChosenSocketName, const FString\u0026amp; TheIP, const int32 ThePort, bool UDP) { RemoteAddr = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)-\u0026gt;CreateInternetAddr(); bool bIsValid; RemoteAddr-\u0026gt;SetIp(*TheIP, bIsValid); RemoteAddr-\u0026gt;SetBroadcastAddress(); RemoteAddr-\u0026gt;SetPort(ThePort); if (!bIsValid) { return false; } SenderSocket = FUdpSocketBuilder(*YourChosenSocketName) .AsReusable()//使绑定的地址可以被其他套接字重用 .WithBroadcast() .WithSendBufferSize(2 * 1024 * 1024) /*.BoundToEndpoint(Endpoint)*/ ; check(SenderSocket-\u0026gt;GetSocketType() == SOCKTYPE_Datagram); int32 SendSize = 2 * 1024 * 1024; SenderSocket-\u0026gt;SetSendBufferSize(SendSize, SendSize); SenderSocket-\u0026gt;SetReceiveBufferSize(SendSize, SendSize); if (bIsValid) { bIsValid = true; } return bIsValid; } bool AUDPSend::RamaUDPSender_SendString(FString ToSend) { if (!SenderSocket) { //ScreenMsg(\u0026#34;No sender socket\u0026#34;); return false; } int32 BytesSent = 0; FString serialized = ToSend; TCHAR* serializedChar = serialized.GetCharArray().GetData(); int32 size = FCString::Strlen(serializedChar); int32 sent = 0; SenderSocket-\u0026gt;SendTo((uint8*)TCHAR_TO_UTF8(serializedChar), size, BytesSent, *RemoteAddr);//发送给远端地址\nif (BytesSent \u0026lt;= 0) {\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 const FString Str = \u0026#34;Socket is valid but the receiver received 0 bytes, make sure it is listening properly!\u0026#34;; UE_LOG(LogTemp, Error, TEXT(\u0026#34;%s\u0026#34;), *Str); ScreenMsg(Str); return false; } return true; } void AUDPSend::EndPlay(const EEndPlayReason::Type EndPlayReason) { Super::EndPlay(EndPlayReason); if (SenderSocket) { SenderSocket-\u0026gt;Close(); ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)-\u0026gt;DestroySocket(SenderSocket); } } FString AUDPSend::GetIP() { bool canBind = false; TSharedRef\u0026lt;FInternetAddr\u0026gt; localIP = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)-\u0026gt;GetLocalHostAddr(*GLog, canBind); // 获得本机的IP FString ipStr = (localIP-\u0026gt;IsValid() ? localIP-\u0026gt;ToString(false) : \u0026#34;\u0026#34;); return ipStr; } //UDPRecive.h #pragma once #include \u0026#34;CoreMinimal.h\u0026#34; #include \u0026#34;GameFramework/Actor.h\u0026#34; #include \u0026#34;Runtime/Networking/Public/Networking.h\u0026#34; #include \u0026#34;UDPRecive.generated.h\u0026#34; UCLASS() class UDPTEST426_API AUDPRecive : public AActor { GENERATED_BODY() public: // Sets default values for this actor\u0026#39;s properties AUDPRecive(); protected: // Called when the game starts or when spawned virtual void BeginPlay() override; public: // Called every frame virtual void Tick(float DeltaTime) override; public: FSocket* ListenSocket; FUdpSocketReceiver* UDPReceiver = nullptr; TSharedPtr\u0026lt;FInternetAddr\u0026gt; RemoteAddr; UFUNCTION(BlueprintCallable, Category = \u0026#34;UDP\u0026#34;) void StartUDPReceiver(const FString\u0026amp; YourChosenSocketName, const FString\u0026amp; TheIP, const int32 ThePort, bool\u0026amp; success); //接受消息 UFUNCTION(BlueprintPure, Category = \u0026#34;UDP\u0026#34;) void DataRecv(FString\u0026amp; str, bool\u0026amp; success); FORCEINLINE void ScreenMsg(const FString\u0026amp; Msg) { GEngine-\u0026gt;AddOnScreenDebugMessage(-1, 5.f, FColor::Red, *Msg); } FORCEINLINE void ScreenMsg(const FString\u0026amp; Msg, const float Value) { GEngine-\u0026gt;AddOnScreenDebugMessage(-1, 5.f, FColor::Red, FString::Printf(TEXT(\u0026#34;%s %f\u0026#34;), *Msg, Value)); } FORCEINLINE void ScreenMsg(const FString\u0026amp; Msg, const FString\u0026amp; Msg2) { GEngine-\u0026gt;AddOnScreenDebugMessage(-1, 5.f, FColor::Red, FString::Printf(TEXT(\u0026#34;%s %s\u0026#34;), *Msg, *Msg2)); } public: /** Called whenever this actor is being removed from a level */ virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; }; //UDPRecive.cpp // Fill out your copyright notice in the Description page of Project Settings. #include \u0026#34;UDPRecive.h\u0026#34; // Sets default values AUDPRecive::AUDPRecive() { // Set this actor to call Tick() every frame. You can turn this off to improve performance if you don\u0026#39;t need it. PrimaryActorTick.bCanEverTick = true; ListenSocket = NULL; } // Called when the game starts or when spawned void AUDPRecive::BeginPlay() { Super::BeginPlay(); } // Called every frame void AUDPRecive::Tick(float DeltaTime) { Super::Tick(DeltaTime); } //结束时出发事件 void AUDPRecive::EndPlay(const EEndPlayReason::Type EndPlayReason) { Super::EndPlay(EndPlayReason); //UDPReceiver置空 delete UDPReceiver; UDPReceiver = nullptr; //Clear all sockets! if (ListenSocket) { ListenSocket-\u0026gt;Close(); ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)-\u0026gt;DestroySocket(ListenSocket); } } //初始化Receiver void AUDPRecive::StartUDPReceiver(const FString\u0026amp; YourChosenSocketName, const FString\u0026amp; TheIP, const int32 ThePort, bool\u0026amp; success) { //TSharedRef\u0026lt;FInternetAddr\u0026gt; targetAddr = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)-\u0026gt;CreateInternetAddr(); FIPv4Address Addr; FIPv4Address::Parse(TheIP, Addr); //使用指定的NetID和端口创建并初始化新的IPv4端点。 // Any: Defines the wild card endpoint, which is 0.0.0.0:0 FIPv4Endpoint Endpoint(FIPv4Address::Any, ThePort); //所有ip地址本地 //FUdpSocketBuilder: Implements a fluent builder for UDP sockets. ListenSocket = FUdpSocketBuilder(*YourChosenSocketName) .AsNonBlocking()//将套接字操作设置为非阻塞。 这个实例（用于方法链）。 .AsReusable()//使绑定的地址可以被其他套接字重用。 这个实例（用于方法链）。 .BoundToEndpoint(Endpoint)//设置将端口绑定到本地端点。 这个实例（用于方法链）。 .WithReceiveBufferSize(2 * 1024 * 1024)//设置接收数据大小 ; //BUFFER SIZE int32 BufferSize = 2 * 1024 * 1024; ListenSocket-\u0026gt;SetSendBufferSize(BufferSize, BufferSize); ListenSocket-\u0026gt;SetReceiveBufferSize(BufferSize, BufferSize);\nif (ListenSocket) { success = true; } else { ScreenMsg(\u0026ldquo;No socket\u0026rdquo;); success = false; }\n//return true; }\n1 2 3 4 5 6 7 8 9 10 void AUDPRecive::DataRecv(FString\u0026amp; str, bool\u0026amp; success) { if (!ListenSocket) { ScreenMsg(\u0026#34;No sender socket\u0026#34;); success = false; } TSharedRef\u0026lt;FInternetAddr\u0026gt; targetAddr = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)-\u0026gt;CreateInternetAddr(); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 TArray\u0026lt;uint8\u0026gt; ReceivedData;//定义一个接收器 uint32 Size; //ListenSocket-\u0026gt;HasPendingData(Size) 查询套接字以确定队列中是否有挂起的数据，如果套接字有数据，则为true，否则为false Size参数指示单个recv调用的管道上有多少数据 if (ListenSocket-\u0026gt;HasPendingData(Size)) { success = true; str = \u0026#34;\u0026#34;; uint8* Recv = new uint8[Size]; int32 BytesRead = 0; //将数组调整到给定数量的元素。 新元素将被初始化。 ReceivedData.SetNumUninitialized(FMath::Min(Size, 65507u)); ListenSocket-\u0026gt;RecvFrom(ReceivedData.GetData(), ReceivedData.Num(), BytesRead, *targetAddr); char ansiiData[1024]; memcpy(ansiiData, ReceivedData.GetData(), BytesRead);//拷贝数据到接收器 ansiiData[BytesRead] = 0; //判断数据结束\n1 2 3 4 5 6 7 8 9 FString debugData = ANSI_TO_TCHAR(ansiiData); //字符串转换 str = debugData; // memset(ansiiData,0,1024);//清空 } else { success = false; } } ","permalink":"https://imrcao.top/posts/%E5%BD%92%E6%A1%A3/%E4%BD%BF%E7%94%A8ue4%E5%8E%9F%E7%94%9F%E6%A8%A1%E5%9D%97%E8%BF%9B%E8%A1%8C%E7%AE%80%E5%8D%95udp%E5%B9%BF%E6%92%AD%E5%92%8C%E6%95%B0%E6%8D%AE%E6%8E%A5%E6%94%B6/","summary":"前言： 最近在学习UE4网络相关知识，尝试学习使用 UDP做一个分布式服务器（后面会出相关文章），这里简单记录一下如何使用 UE4的“Sockets”模块和“Networking”模块进行UDP广播。\n操作步骤： 1、首先创建两个空的UE4C++工程（我这里使用的是UE426版本引擎），一个用来广播数据，一个用来接收数据，在两个工程分别创建继承自UObject的类，分别命名为“ UDPSend”和“ UDPRecive”，还需在\u0026quot;.build.cs\u0026quot;文件中添加模块“Sockets\u0026quot;, \u0026ldquo;Networking”，示例如下代码所示。 2、为了清晰可见，直接上代码（部分代码是网上粘贴的，我这里只是想记录的同时再复习一遍，也方便自己后续查阅）。 3、编译后启动引擎，将两个基于C++的蓝图类，分别拖入到场景中（用来广播的工程只用创建UDPSend，用来接收的工程只用创建“UDPRecive”）。拖入场景中后，在UDPSend中调用函数“StartUDPSender()”，传入网络套接字、 IP、端口号即可。UDP开启后，可以TICK执行广播数据（调用函数RamaUDPSender_SendString（））；接收数据的工程也是一样，将对应类拖入场景中，打开蓝图编辑器，先调用函数StartUDPReceiver（），再调用函数DataRecv（）。两个工程要同时运行，这样就能广播和接收数据了。 using UnrealBuildTool;\npublic class UDPTest426 : ModuleRules { public UDPTest426(ReadOnlyTargetRules Target) : base(Target) { PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 PublicDependencyModuleNames.","title":"使用UE4原生模块进行简单UDP广播和数据接收"},{"content":"前言 在本指南中你将了解如何使用Perforec管理UE项目，包括Perforce部署、安装、使用。我使用了家中电脑和公司电脑作为两个客户端，还有一台腾讯云服务器作用来托管Perforce。\n下载 在Perforce官网（https://www.perforce.com/downloads）下载Perforce的服务器（Helix Core(P4D) Server）和客户端（Helix Visual Client(P4V)），直接搜英文名下载即可。\n部署Perforce服务器 使用PC自带的远程桌面登录访问腾讯云服务器，将下载好的perforce服务器安装包拷贝到腾讯云上或者直接在腾讯云上下载。双击安装包运行，如图所示；\n默认端口号为1666，如果和你本地端口冲突也可以自定义端口。点击“Change”选择一个用来存放资产的目录，如下图所示；\n自定义你的User Name，客户端连接的服务器的时候会用到这个User Name。安装完毕后点击“exit”即可。如下图所示；\n检测Perforce服务器是否运行成功。在命令行工具中输入“netstat -a\u0026quot;回车，会发现有端口为1666的程序在运行，说明Perforce安装部署成功。如下图所示；\n安装Perforce客户端 双击安装包，默认全部勾选，选择安装路径后，点击”Next“。如图所示：\n输入你的服务器公网IP地址和端口号（x.x.x.x:1666）以及在服务器中刚刚输入的User Name，最后一栏默认选择即可，点击”Next“。\n创建一个”User“和”Workspace“。创建用户时可以不用输入密码。如下图所示。\n创建好用户和工作空间后点击OK，会弹出来一个窗口让你上传文件到服务器。你可以指定一个文件夹或者创建一个文件夹，文件夹内需要有资产。如下图所示。\n我这里选择”Use a classic depot“，看自己需要。如下图。\n简单使用 点击”Get Latest“获取服务器最新资产；点击”Checkout“检出需要操作的资产；点击”Add“添加新的资产；点击”Submit“提交修改的资产； 使用 Unreal Engine 连接 Perforce 服务器 直接在Perforce中打开Unreal项目，在编辑器的右下角找到“Source Control”，点击选择“”，在弹出来的面板中输入服务器IP和端口，以及本地Perforce用户名和工作空间的名称，点击“Accept Settings”即可。\n其它 Tips 1、当你不小心添加了很多无用的文件到DefaultList中，可以右键单击某一个Change列表，选择Revert Files，恢复。 ","permalink":"https://imrcao.top/posts/0.6%E4%BD%BF%E7%94%A8perforce%E7%AE%A1%E7%90%86unreal-engine%E9%A1%B9%E7%9B%AE/","summary":"前言 在本指南中你将了解如何使用Perforec管理UE项目，包括Perforce部署、安装、使用。我使用了家中电脑和公司电脑作为两个客户端，还有一台腾讯云服务器作用来托管Perforce。\n下载 在Perforce官网（https://www.perforce.com/downloads）下载Perforce的服务器（Helix Core(P4D) Server）和客户端（Helix Visual Client(P4V)），直接搜英文名下载即可。\n部署Perforce服务器 使用PC自带的远程桌面登录访问腾讯云服务器，将下载好的perforce服务器安装包拷贝到腾讯云上或者直接在腾讯云上下载。双击安装包运行，如图所示；\n默认端口号为1666，如果和你本地端口冲突也可以自定义端口。点击“Change”选择一个用来存放资产的目录，如下图所示；\n自定义你的User Name，客户端连接的服务器的时候会用到这个User Name。安装完毕后点击“exit”即可。如下图所示；\n检测Perforce服务器是否运行成功。在命令行工具中输入“netstat -a\u0026quot;回车，会发现有端口为1666的程序在运行，说明Perforce安装部署成功。如下图所示；\n安装Perforce客户端 双击安装包，默认全部勾选，选择安装路径后，点击”Next“。如图所示：\n输入你的服务器公网IP地址和端口号（x.x.x.x:1666）以及在服务器中刚刚输入的User Name，最后一栏默认选择即可，点击”Next“。\n创建一个”User“和”Workspace“。创建用户时可以不用输入密码。如下图所示。\n创建好用户和工作空间后点击OK，会弹出来一个窗口让你上传文件到服务器。你可以指定一个文件夹或者创建一个文件夹，文件夹内需要有资产。如下图所示。\n我这里选择”Use a classic depot“，看自己需要。如下图。\n简单使用 点击”Get Latest“获取服务器最新资产；点击”Checkout“检出需要操作的资产；点击”Add“添加新的资产；点击”Submit“提交修改的资产； 使用 Unreal Engine 连接 Perforce 服务器 直接在Perforce中打开Unreal项目，在编辑器的右下角找到“Source Control”，点击选择“”，在弹出来的面板中输入服务器IP和端口，以及本地Perforce用户名和工作空间的名称，点击“Accept Settings”即可。\n其它 Tips 1、当你不小心添加了很多无用的文件到DefaultList中，可以右键单击某一个Change列表，选择Revert Files，恢复。 ","title":"使用Perforce管理Unreal Engine项目"},{"content":"一、创建私有仓库 这一步不详细记录 注意：创建仓库的时候选择添加gitIgnore文件，文件模板选择“UnrealEngine”\n二、随便传几个文件 三、创建SSH密钥（核心） 使用SSH密钥克隆仓库，首要需要创建SSH密钥。 创建SSH密钥入口：点击头像选中“Settings” 根据链接中的提示创建 链接：https://docs.github.com/zh/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent\n安装Git for Windows（它自带 Git Bash） 管理员运行cmd，执行：\n1 git-installer.exe /VERYSILENT /NORESTART 安装验证：\n1 git --version 首先打开Git Bash（可以从SourceTree中启动） 先执行：\n1 ssh-keygen -t ed25519 -C \u0026#34;your_email@example.com\u0026#34; //这里换成自己的邮箱 接着会出来：\n1 Enter file in which to save the key (/c/Users/YOU/.ssh/id_ALGORITHM):[Press enter] //直接点回车就行 接着会让你输入两次密码：（19960525）\n1 2 Enter passphrase (empty for no passphrase): [Type a passphrase] Enter same passphrase again: [Type passphrase again] 最终命令行如下： 结果会生成两个文件： 将这个key放到刚才在GitHub创建ssH文件的地方：\n这样就可以复制ssh链接准备开始克隆了。\n四、安装sourceTree（下载），安装过程中可能需要注册一下账号.. 这里打开终端： 输入ssh链接，指定目录克隆即可。 五、指定需要版本控制的文件 一般来说传这几个文件\n根据需要配置额外的 Ignore 文件，示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Content/Assets/* # 代表忽略Content/Assets/路径下的所有文件。 *.sln # 代表忽略指定文件格式 # BinaryFiles Binaries/* # Plugins Files by imrcao Plugins/**/Binaries/* Plugins/**/Intermediate/* Plugins/MeshOps/* Plugins/RuntimeFBXImport/* # Content Files by imrcao Content/Assets/* Content/Collections/* Content/Developers/* ","permalink":"https://imrcao.top/posts/0.5github%E9%85%8D%E5%90%88sourcetree%E7%AE%A1%E7%90%86ue4%E5%B7%A5%E7%A8%8B/","summary":"一、创建私有仓库 这一步不详细记录 注意：创建仓库的时候选择添加gitIgnore文件，文件模板选择“UnrealEngine”\n二、随便传几个文件 三、创建SSH密钥（核心） 使用SSH密钥克隆仓库，首要需要创建SSH密钥。 创建SSH密钥入口：点击头像选中“Settings” 根据链接中的提示创建 链接：https://docs.github.com/zh/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent\n安装Git for Windows（它自带 Git Bash） 管理员运行cmd，执行：\n1 git-installer.exe /VERYSILENT /NORESTART 安装验证：\n1 git --version 首先打开Git Bash（可以从SourceTree中启动） 先执行：\n1 ssh-keygen -t ed25519 -C \u0026#34;your_email@example.com\u0026#34; //这里换成自己的邮箱 接着会出来：\n1 Enter file in which to save the key (/c/Users/YOU/.ssh/id_ALGORITHM):[Press enter] //直接点回车就行 接着会让你输入两次密码：（19960525）\n1 2 Enter passphrase (empty for no passphrase): [Type a passphrase] Enter same passphrase again: [Type passphrase again] 最终命令行如下： 结果会生成两个文件： 将这个key放到刚才在GitHub创建ssH文件的地方：\n这样就可以复制ssh链接准备开始克隆了。\n四、安装sourceTree（下载），安装过程中可能需要注册一下账号.. 这里打开终端： 输入ssh链接，指定目录克隆即可。 五、指定需要版本控制的文件 一般来说传这几个文件","title":"Github配合Sourcetree管理UE4工程"},{"content":"这里记录程序线执行流程\n一、pak资产挂载流程\n目前有两个挂载入口：\n0.1加载本地pak资产 /Game/Blueprints/UI/CoreUI/WPT_AssetLibraryMenu.WPT_AssetLibraryMenu蓝图中ConstructSkuClass函数调用void UCPT_UnitSkuButton::RequestMountPak(const FString\u0026amp; LocalPakPath)函数进行pak挂载请求；\n0.2下载云端资产 void FCPT_AssetDownloadService::OnDownloadFinished函数中调用void UCPT_UnitSkuButton::RequestMountPak(const FString\u0026amp; LocalPakPath)函数进行pak挂载请求。\n挂载委托关系梳理：\nUCPT_UnitSkuButton类 有代理：OnMountPakRequested（声明：DECLARE_DELEGATE_TwoParams(FOnMountPakRequested, UCPT_UnitSkuButton*, const FString\u0026amp;);） 有函数： void UCPT_UnitSkuButton::RequestMountPak(const FString\u0026amp; LocalPakPath) { OnMountPakRequested.ExecuteIfBound(this, LocalPakPath); }\nUCPT_AssetLibraryMenu类 有代理：DE_MountPakRequested（声明DECLARE_DELEGATE_TwoParams(FOnPakMountRequested, UCPT_UnitSkuButton*, const FString\u0026amp;)） 有函数： void UCPT_AssetLibraryMenu::RegisterSkuButton(UCPT_UnitSkuButton* Button) { Button-\u0026gt;OnMountPakRequested.BindLambda([this](UCPT_UnitSkuButton* Btn, const FString\u0026amp; PakPath) { DE_MountPakRequested.ExecuteIfBound(Btn, PakPath); }); }\nUCPT_MainUI类 在函数void UCPT_MainUI::LoadLocalAssets()中进行了绑定： LeftMenuBar-\u0026gt;AssetLibraryMenu-\u0026gt;DE_MountPakRequested.BindUObject(this, \u0026amp;UCPT_MainUI::HandlePakMountRequested);\n流程如下： 1、首先会在函数void UCPT_MainUI::LoadLocalAssets()中绑定DE_MountPakRequested代理。\n2、然后在构建资产缩略图按钮时会调用RegisterSkuButton注册代理。\n请求挂载：RequestMountPak -\u0026gt; 由于在（2）中绑定了lambda函数，所以会执行DE_MountPakRequested -\u0026gt; 由于在（1）中绑定代理，所以最终执行：HandlePakMountRequested。\n流程图/拓扑图\n整体挂载流程拓扑图 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 ┌─────────────────────────────────────────────────────────────────────────────┐ │ Pak 资产挂载流程总览 │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ 入口A：启动时加载本地pak 入口B：下载云端资产 │ │ ┌─────────────────────┐ ┌──────────────────────┐ │ │ │ UCPT_MainUI:: │ │ UCPT_UnitSkuButton:: │ │ │ │ LoadLocalAssets() │ │ RequestDownload() │ │ │ └─────────┬───────────┘ └──────────┬───────────┘ │ │ │ │ │ │ │ 读取本地清单 入队下载 │ │ │ │ manifest_cache.json FCPT_AssetDownloadService │ │ │ ::EnqueueDownload() │ │ │ │ │ │ │ ┌─────────▼──────────┐ │ │ │ │ 线程池异步下载 │ │ │ │ │ ExecuteDownload() │ │ │ │ │ (HTTP GET .pak文件) │ │ │ │ └─────────┬──────────┘ │ │ │ │ │ │ │ 下载完成回调 │ │ │ │ │ │ │ │ ┌─────────▼──────────┐ │ │ │ │ OnDownloadFinished()│ │ │ │ │ 设置状态 Mounting │ │ │ │ └─────────┬──────────┘ │ │ │ │ │ │ │ ┌─────────────────────────────────────┘ │ │ │ │ 两种路径在此汇合 │ │ ▼ ▼ │ │ ┌─────────────────────────┐ │ │ │ UCPT_UnitSkuButton:: │ │ │ │ RequestMountPak() │ │ │ └────────────┬────────────┘ │ │ │ 触发 OnMountPakRequested 委托 │ │ ▼ │ │ ┌─────────────────────────┐ lambda 转发 ┌────────────────────────┐ │ │ │ AssetLibraryMenu:: │ ──────────────► │ DE_MountPakRequested │ │ │ │ RegisterSkuButton() │ └───────────┬────────────┘ │ │ └─────────────────────────┘ │ │ │ │ 委托绑定 │ │ ▼ │ │ ┌──────────────────────────────────────────────────────────────────┐ │ │ │ UCPT_MainUI::HandlePakMountRequested(Button, LocalPakPath) │ │ │ └───────────────────────────┬──────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────────────────────────────────┐ │ │ │ UCPT_MainUI::MountPakFiles() （底层挂载核心） │ │ │ │ 1. UPakLoaderLibrary::IsValidPakFile() 验证pak文件 │ │ │ │ 2. UPakLoaderLibrary::MountPakFile() 挂载pak文件 │ │ │ │ 3. RegisterMountPoint(\u0026#34;/PTPakDLC/\u0026#34;) 注册虚拟挂载点 │ │ │ │ 4. LoadPakAssetRegistryFile() 加载资产注册表 │ │ │ │ 5. GetFilesInPak() 枚举pak内文件 │ │ │ │ 6. 过滤 \u0026#34;Blueprints/Sku\u0026#34; 路径 → 得到 GameClassPath │ │ │ └───────────────────────────┬──────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────────────────────────────────┐ │ │ │ UCPT_UnitSkuButton::HandleMountComplete(bSuccess, GameClassPath)│ │ │ │ │ │ │ │ 成功时： │ │ │ │ 1. 保存 EntryInfo.GameClassPath / bDownloaded = true │ │ │ │ 2. SetButtonState(Downloaded) │ │ │ │ 3. 持久化到 manifest_cache.json │ │ │ │ 4. CastBaseClass(GameClassPath) 通知蓝图设置基类路径 │ │ │ │ │ │ │ │ 失败时： │ │ │ │ SetButtonState(NeedsUpdate) │ │ │ └──────────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────────┘ 委托链路图 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 SkuButton AssetLibraryMenu MainUI ────────── ───────────────── ─────── RequestMountPak() │ │ ExecuteIfBound ▼ OnMountPakRequested ─────► [lambda 中转] ─────► DE_MountPakRequested ─────► HandlePakMountRequested() │ │ │ 绑定时机： │ │ LoadLocalAssets() │ │ 中 BindUObject │ ▼ MountPakFiles() │ ▼ HandleMountComplete() 资产按钮状态机 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 ┌────────────────┐ │ NotDownloaded │ ◄──────────────────────────────────┐ └───────┬────────┘ │ │ 用户点击角标 │ ▼ │ ┌────────────────┐ │ │ Queued │ │ └───────┬────────┘ │ │ 线程池开始下载 │ ▼ │ ┌────────────────┐ │ │ Downloading │ ◄── UpdateDownloadProgress() │ └───────┬────────┘ 更新下载进度 │ │ 下载完成 │ ▼ │ ┌────────────────┐ │ │ Mounting │ │ └───────┬────────┘ │ ┌─────┴─────┐ │ │ │ │ 成功│ 失败│ │ ▼ ▼ │ ┌────────────┐ ┌──────────────┐ │ │ Downloaded │ │ NeedsUpdate │ ──── 下载失败时回到 ─────────┘ └────────────┘ └──────────────┘ OnDownloadFailed() ","permalink":"https://imrcao.top/projects/0.1%E9%A2%84%E6%BC%94%E7%B3%BB%E7%BB%9F-pak%E8%B5%84%E4%BA%A7%E6%8C%82%E8%BD%BD%E6%B5%81%E7%A8%8B%E8%AE%B0%E5%BD%95/","summary":"这里记录程序线执行流程\n一、pak资产挂载流程\n目前有两个挂载入口：\n0.1加载本地pak资产 /Game/Blueprints/UI/CoreUI/WPT_AssetLibraryMenu.WPT_AssetLibraryMenu蓝图中ConstructSkuClass函数调用void UCPT_UnitSkuButton::RequestMountPak(const FString\u0026amp; LocalPakPath)函数进行pak挂载请求；\n0.2下载云端资产 void FCPT_AssetDownloadService::OnDownloadFinished函数中调用void UCPT_UnitSkuButton::RequestMountPak(const FString\u0026amp; LocalPakPath)函数进行pak挂载请求。\n挂载委托关系梳理：\nUCPT_UnitSkuButton类 有代理：OnMountPakRequested（声明：DECLARE_DELEGATE_TwoParams(FOnMountPakRequested, UCPT_UnitSkuButton*, const FString\u0026amp;);） 有函数： void UCPT_UnitSkuButton::RequestMountPak(const FString\u0026amp; LocalPakPath) { OnMountPakRequested.ExecuteIfBound(this, LocalPakPath); }\nUCPT_AssetLibraryMenu类 有代理：DE_MountPakRequested（声明DECLARE_DELEGATE_TwoParams(FOnPakMountRequested, UCPT_UnitSkuButton*, const FString\u0026amp;)） 有函数： void UCPT_AssetLibraryMenu::RegisterSkuButton(UCPT_UnitSkuButton* Button) { Button-\u0026gt;OnMountPakRequested.BindLambda([this](UCPT_UnitSkuButton* Btn, const FString\u0026amp; PakPath) { DE_MountPakRequested.ExecuteIfBound(Btn, PakPath); }); }\nUCPT_MainUI类 在函数void UCPT_MainUI::LoadLocalAssets()中进行了绑定： LeftMenuBar-\u0026gt;AssetLibraryMenu-\u0026gt;DE_MountPakRequested.BindUObject(this, \u0026amp;UCPT_MainUI::HandlePakMountRequested);\n流程如下： 1、首先会在函数void UCPT_MainUI::LoadLocalAssets()中绑定DE_MountPakRequested代理。\n2、然后在构建资产缩略图按钮时会调用RegisterSkuButton注册代理。\n请求挂载：RequestMountPak -\u0026gt; 由于在（2）中绑定了lambda函数，所以会执行DE_MountPakRequested -\u0026gt; 由于在（1）中绑定代理，所以最终执行：HandlePakMountRequested。\n流程图/拓扑图\n整体挂载流程拓扑图 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 ┌─────────────────────────────────────────────────────────────────────────────┐ │ Pak 资产挂载流程总览 │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ 入口A：启动时加载本地pak 入口B：下载云端资产 │ │ ┌─────────────────────┐ ┌──────────────────────┐ │ │ │ UCPT_MainUI:: │ │ UCPT_UnitSkuButton:: │ │ │ │ LoadLocalAssets() │ │ RequestDownload() │ │ │ └─────────┬───────────┘ └──────────┬───────────┘ │ │ │ │ │ │ │ 读取本地清单 入队下载 │ │ │ │ manifest_cache.","title":"预演系统-Pak资产挂载流程记录"},{"content":"克隆命令\n1 git clone GitHub地址 条件：首先需要魔法上网，否则国内是无法连接GitHub的。其次需要设置代理的端口，否则也会提示连接失败。\n1 2 3 git clone https://github.com/neilsonnn/image-blaster \u0026#34;F:/00_Imrcao/01_MyProject/ImageBlaster\u0026#34; Cloning into \u0026#39;F:/00_Imrcao/01_MyProject/ImageBlaster\u0026#39;... fatal: unable to access \u0026#39;https://github.com/neilsonnn/image-blaster/\u0026#39;: Failed to connect to github.com p 设置代理端口\n1 2 git config --global http.proxy http://127.0.0.1:\u0026lt;端口\u0026gt; git config --global https.proxy http://127.0.0.1:\u0026lt;端口\u0026gt; 清除代理端口：\n1 2 git config --global --unset http.proxy git config --global --unset https.proxy 查看代理端口:\n1 2 git config --global --get http.proxy git config --global --get https.proxy 常用git命令 初始化与首次提交\n1 2 3 git init # 在当前目录初始化 Git 仓库 git add . # 将所有文件添加到暂存区 git commit -m \u0026#34;Initial commit\u0026#34; # 创建首次提交 关联远程仓库\n1 2 3 git remote add origin \u0026lt;url\u0026gt; # 关联远程仓库 git branch -M main # 将当前分支重命名为 main git push -u origin main # 推送并设置上游追踪 日常操作\n1 2 3 4 5 6 7 8 9 git status # 查看工作区和暂存区状态 git status -s # 短格式输出 git status -uall # 显示所有未跟踪的文件（和sourcetree上的“未暂存文件”显示的一样） git diff # 查看未暂存的修改 git diff --staged # 查看已暂存的修改 git add \u0026lt;file\u0026gt; # 添加指定文件到暂存区 git add . # 添加所有文件（未暂存）到暂存区 git commit -m \u0026#34;message\u0026#34; # 提交暂存的修改 git log --oneline # 查看简洁提交历史 分支操作\n1 2 3 4 5 git branch # 列出本地分支 git branch \u0026lt;name\u0026gt; # 创建新分支 git checkout \u0026lt;name\u0026gt; # 切换分支 git checkout -b \u0026lt;name\u0026gt; # 创建并切换到新分支 git merge \u0026lt;name\u0026gt; # 将指定分支合并到当前分支 远程同步\n1 2 3 git fetch # 拉取远程更新（不合并） git pull # 拉取并合并远程更新 git push # 推送本地提交到远程 撤销与回退\n1 2 3 4 git restore \u0026lt;file\u0026gt; # 撤销工作区的修改（未暂存） git restore --staged \u0026lt;file\u0026gt; # 取消暂存（保留修改） git stash # 临时保存未提交的修改 git stash pop # 恢复之前 stash 的修改 如果需要为当前项目 (Low_Poly_Game) 执行 git init，我可以帮你操作并创建 .gitignore 文件来排除 UE 的中间文件和编译产物。\n","permalink":"https://imrcao.top/posts/0.3%E4%BD%BF%E7%94%A8git%E5%91%BD%E4%BB%A4%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98/","summary":"克隆命令\n1 git clone GitHub地址 条件：首先需要魔法上网，否则国内是无法连接GitHub的。其次需要设置代理的端口，否则也会提示连接失败。\n1 2 3 git clone https://github.com/neilsonnn/image-blaster \u0026#34;F:/00_Imrcao/01_MyProject/ImageBlaster\u0026#34; Cloning into \u0026#39;F:/00_Imrcao/01_MyProject/ImageBlaster\u0026#39;... fatal: unable to access \u0026#39;https://github.com/neilsonnn/image-blaster/\u0026#39;: Failed to connect to github.com p 设置代理端口\n1 2 git config --global http.proxy http://127.0.0.1:\u0026lt;端口\u0026gt; git config --global https.proxy http://127.0.0.1:\u0026lt;端口\u0026gt; 清除代理端口：\n1 2 git config --global --unset http.proxy git config --global --unset https.proxy 查看代理端口:\n1 2 git config --global --get http.proxy git config --global --get https.proxy 常用git命令 初始化与首次提交\n1 2 3 git init # 在当前目录初始化 Git 仓库 git add .","title":"使用Git命令常见问题"},{"content":"一、引言：为什么选择这套工作流？ 痛点分析：\n我之前使用的是wordpress + WampServer + 阿里云创建的网站，这个流程最大的缺点就是在编辑笔记时需要去wordpress后台，后台不仅慢，编辑起来还费劲，虽然也可以在Obsidian中配合wordpress插件，但总还是感觉wordpress臃肿，不仅如此，服务器使用的是轻量级的，只有2M的带宽，访问很慢，而且域名备案流程繁琐且缓慢。接下来我介绍的流程可以免费托管，只需要为域名付费即可，而且域名无需在国内备案。\n我的终极解法：\n一套本地丝滑写作、云端自动构建、且几乎零成本的现代化技术博客方案。\n核心技术栈预览：\nObsidian (本地编辑) + PicGo (图床管理) + Hugo (静态生成) + GitHub (版本控制) + Vercel (全球部署)。\n二、 构建本地终极写作环境 (Obsidian 篇) 1. 认识 Obsidian\n为什么它是技术笔记的神器（Markdown 原生、双链、极速响应），在遇到Obsidian之前我使用的是有道云和xmind，随着笔记内容越来越多有道云启动都要半天，现在还有好多广告。Obsidian完全本地化管理，数据安全，社区插件生态丰富，可以创建白板，一定程度上可以取代xmind。下载链接：https://obsidian.md/zh/\n2. 图文分离：优雅的图片管理方案 (PicGo)\n（1）、本地图片的痛点（分享不便、打包体积大）。技术笔记里面通常有很多图片，当积累一定规模时，无论时分享还是传到Github上进行云端备份都是一个棘手的问题。如果将笔记部署到网站里，那么图片多的时候往往加载很慢，对服务器带宽是一个极大的考验。所以图文分离是一个很有必要的步骤。\n（2）、配置阿里云OSS对象存储。关于OSS不细述，只讲几个关键点。首先是关于AccessKey的，点击头像找到“权限与安全-\u0026gt;AccessKey”，我这里使用RAM用户AccessKey，如图1所示。然后根据配置执行生成即可，保存好你的AccessID和Key，只显示一次。然后进入对象存储OSS控制页面，先创建一个bucket，点击创建的bucket，找到权限控制-\u0026gt;阻止公共访问，点击关闭，如图2所示。在同一个页面，找到读写权限，将“Bucket ACL\u0026quot;设置为公共读。不然当你将图片链接放入笔记中时并不会正常显示图片。OSS有桌面版可视化管理工具，也可以下载使用。\n（3）、配置 PicGo 图床方案（搭配阿里云 OSS 云存储）。首先安装PicGo，Github下载链接：https://github.com/Molunerfinn/PicGo/releases。我下载的是2.5.2版本。如图2-2-2-1所示，我使用的是阿里云的OSS作为图床，填入相关配置后，保存即可。关于\n（4）、在 Obsidian 中实现“复制粘贴，自动上传并返回图床链接”的丝滑体验。这一步可以实现在Obsidian笔记中直接粘贴图片（配合Snipaste截图软件更方便，F1直接截图，Ctrl + V直接复制），自动将图片传到图床，并返回图床的链接。在Obsidian中下载一个叫“Image auto upload”的第三方插件，启用插件后，进入插件设置，设置PicGOServer 上传接口：http://127.0.0.1:36677/upload，一般默认就是这个值，端口号和PicGo中设置的保持一致，如下图所示。\n3. 云端托管与版本控制 (Obsidian Git)\n安装与配置 Obsidian Git 插件。\n（1）、即使不准备搭建个人博客，只要你有记录笔记的习惯，这一部分对你也有很多帮助。Obsidian自带有云端同步功能，而且是端到端加密的，但是需要付费。另一个方案就是将Obsidian仓库上传到Github，这样可以实现在不同的工作地点对同一篇笔记进行无缝编辑。 （2）、首先创建一个Github仓库，然后将仓库克隆到本地，这一步可以参考笔记“Github配合Sourcetree管理UE4工程”（牵扯到SSH密钥）。在Obsidian中点击仓库管理，选择打开本地仓库，将刚才克隆的本地到仓库作为Obsidian的工作空间。在.gitignore文件中可以设置忽略的文件。\n（3）、在Obsidian中下载Git第三方插件，启用后会在右侧出现Git操作面板，如图所示。常用的流程如下，当修改文件后，点击加号（Stage All）暂存所有，然后在输入框（默认Update Bolg：）输入当前修改的内容，接着点击对号（Commit）提交，最后点击上传按钮（push）推送到GitHub仓库。如果是比较小的修改，可以直接点击第一个向上的箭头（Commit-And-Sync）一键提交和推送。\n总结\n到这里已经实现了Obsidian + Picgo + Github的笔记托管方案。如果不考虑创建个人博客的话，这套流程用来写笔记也是非常舒服的，数据同步，免费托管，图文分离。\n三、 引擎启动：搭建网站骨架 (Hugo 篇) 在拥有了 Obsidian 写作环境和 PicGo 图床后，我们需要一个“引擎”将这些 Markdown 文档转换成精美的网页。这个引擎就是 Hugo。\n1. 初识 Hugo\n（1）、什么是静态网站生成器？。传统的网站（如 WordPress）就像一家点餐制餐厅：每当有读者访问，服务器就要临时去数据库里“炒菜”（查询数据、渲染页面），这不仅慢，还容易被黑客通过数据库漏洞攻击。而Hugo这种静态网站生成器则是一家预制菜工厂：在你发布文章时，它会瞬间把你所有的 Markdown 文件“预制”成成千上万个标准的 HTML 网页。\n（2）、为什么它比 WordPress 更适合极客？ ·极速编译：Hugo 号称是世界上最快的 SSG。即使有几千篇文章，它也能在几秒钟内完成全站构建。 ·无数据库漏洞：网站只有 HTML/CSS/JS 静态文件，没有数据库。这意味着没有 SQL 注入风险，安全性极高。 ·版本控制友好：整个网站就是一个文件夹，可以完美配合 Git 进行版本管理 ·零成本/低成本托管：静态文件可以免费托管在 Vercel、GitHub Pages 等平台，不需要支付昂贵的服务器和数据库费用。\n2. 本地环境搭建\n（1）、安装 Hugo 程序。 ·Windows 用户打开 PowerShell，输入：scoop install hugo-extended（安装 extended 版本以支持高级 CSS 处理）。 ·手动安装，从 https://github.com/gohugoio/hugo/releases 下载对应的压缩包，解压到自定义目录（例如 D:\\Software\\hugo）。 ·验证：打开终端输入 hugo version。如果显示版本号，则配置成功。如图所示。\n（2）、创建第一个 Hugo 站点，在你想存放博客的目录下打开终端，输入：hugo new site my-blog。这将自动生成 Hugo 的标准目录结构（content, layouts, static 等）。如图所示。\n3. 下载与配置主题\n（1）、挑选并安装主题：PaperMod，PaperMod 是目前 Hugo 社区最受欢迎的极简风主题，非常适合技术博客。https://github.com/adityatelange/hugo-PaperMod 或者官网地址下载：https://themes.gohugo.io/themes/hugo-papermod/。下载后解压到项目的themes文件夹下，主题文件夹命名为PaperMod。\n（2）、配置 hugo.toml。打开根目录下的 hugo.toml，进行初步配置：\n1 2 3 4 5 6 7 8 9 10 baseURL = \u0026#39;https://yourname.top/\u0026#39; languageCode = \u0026#39;zh-cn\u0026#39; title = \u0026#39;我的技术笔记\u0026#39; theme = \u0026#39;PaperMod\u0026#39; # 必须与 themes 文件夹下的名字一致 # Hugo 原生格式的作者配置（一定要加入下面代码，不然本地服务器会启动失败） [author] name = \u0026#39;Imrcao1\u0026#39; [params] author = \u0026#39;Imrcao1\u0026#39; （3）、在本地启动服务器，cd 到项目目录，使用命令：hugo server -D启动，浏览器进入http://localhost:1313/就可以看到了。如图所示。\n（4）、在项目根目录/content下创建posts文件夹，在此文件夹下可以创建first.md文件，来创建一个篇文章。保存后，使用hugo server -D启动服务，就可以看到文章了。如图所示。\n4. 工作流大一统：Obsidian 变身控制台\n（1）、这是最精妙的一步，我们将把写作和网站管理合二为一。将刚才创建的hugo项目根目录设为 Obsidian 工作空间，打开 Obsidian，选择“打开本地库”，直接选择你的 Hugo 项目根目录。现在，你可以在 Obsidian 的侧边栏直接看到项目的文件夹结构。\n（2）、根据第二部分的内容，将该项目工作空间变成Git的本地仓库。\n（3）、也可以使用命令行将该工作空间变成Git的本地仓库。 ·首先打开power shell，cd到工作目录（cd F:\\00_Imrcao\\01_MyProject\\my-site-test）； ·在根目录下创建.gitignore文件，加入以下内容：public/ resources/ .hugo_build.lock（注意换行），这一步很重要，在前面执行hugo server -D命令时会生成public文件夹，这个是本地网页相关的文件，不能上传，否则会影响后续网站构建。 ·然后根据如下指令开始执行。关联完远程仓库后，执行push，会跳出来一个界面，让你选择GitHub账户，如图所示。有可能push失败，此时可以使用obsidian推送，或者使用sourcetree可以。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # 初始化全新的本地 Git 仓库 git init # 把除了 .gitignore 黑名单之外的所有文件添加到暂存区 git add . # 存档并写上备注 git commit -m \u0026#34;全新初始化博客，包含 PaperMod 主题与第一篇测试笔记\u0026#34; # 将主分支命名为 main git branch -M main # 关联你的远程仓库 git remote add origin https://github.com/你的用户名/my-site-test.git # 推送代码到 GitHub git push -u origin main 四、 自动化部署上线 (GitHub \u0026amp; Vercel) 现在obsidian + GitHub已经打通了，hugo项目也创建完毕了，开始准备导入Vercel进行部署。\n**1. 初识 Vercel\n（1）、Vercel前端开发者最爱的免费托管平台 https://vercel.com/。\n**2. 导入GitHub项目，打开网站后使用Github登录，添加项目，导入Github仓库。在导入设置页面，需要做如下设置，如图所示。\n（1）、将Application Presset设置为hugo。 （2）、 Build and Output Settings（构建与输出设置）因为 Vercel 已经自动识别出你的 Framework Preset是 Hugo，所以这一块通常不需要手动修改。 （3）、 Environment Variables（环境变量）—— 这是重中之重！你必须在这里填写 Hugo 的版本号。Hugo 最新版本号是0.160.1，但最新版 Hugo（如 0.160.1）和 PaperMod 主题之间的有语法兼容性冲突，所以这里使用低版本的。 （4）、点击Deploy进行部署\n（5）、在后台管理页面就可以看到一个网址：my-site-test-eight.vercel.app。Vercel 免费为你分配的 .vercel.app 默认域名，目前在大陆网络环境下处于被全线 DNS 污染和屏蔽的状态。因此，如果不开启梯子，国内网络是无法解析并访问这个地址的。\n（6）、彻底解决的完美方案：绑定自定义域名。想要让国内用户（包括没开梯子的你自己）顺利访问，唯一且最专业的方案就是：购买并绑定一个属于你自己的专属域名（例如 yourname.com 或 my-tech-blog.me）。一旦你使用了自定义域名，你的博客将具备以下绝佳优势：\n国内直连免梯子：只要你的自定义域名没有违规历史，国内网络可以直接顺利解析，无需代理即可访问。 完全免备案：因为 Vercel 的服务器物理节点依然在海外，只要你不把域名解析到国内的云服务器，就不需要走繁琐的国内工信部备案流程。 Vercel 的大陆专属加速优化：Vercel 官方甚至专门为大陆网络环境提供了一个优化的 CNAME 解析节点（cname-china.vercel-dns.com）。当你绑定自定义域名时，使用这个特定的记录值，国内访问速度会得到显著提升。 五、 域名与上线 1.购买域名： 在阿里云或者腾讯云上选择一个自己喜欢的域名购买即可，等待注册局审核通过，一般几个小时就可以。\n**2.解析域名\n进入阿里云（或你购买域名的平台），找到“域名解析”，添加两条记录，如图所示。 添加第一条记录： 记录类型：A 主机记录：@ 记录值：76.76.21.21（这是 Vercel 的官方 IP）\n添加第二条记录： 记录类型：CNAME 主机记录：www 记录值：cname-china.vercel-dns.com（注意：这里用了专为中国优化的地址）。\n3.为什么要用 cname-china？：默认的 cname.vercel-dns.com 走的是 Anycast 网络，在国内有时会被干扰或绕路美国；而 cname-china.vercel-dns.com 针对中国电信、联通、移动的线路做了更好的链路优化。\n4.在Vercel后台添加域名：在网页控制台点击Domain，点击Add Existing添加域名（要等域名解析完成）。\n5.修改你的hugo.toml文件：设置文件中的baseURL = https://imrcao.top\n六、定制化：CSS与UI打磨 1.全局视觉 ：创建custom.css文件\n在 项目根目录/assets/css/extended/custom.css， custom.css文件控制着全局的外观。\n七、结语 到这里就个人博客就搭建完毕了。现在工作流程变成了：在Obsidian中编写笔记 -\u0026gt;粘贴图片（自动上传图床）-\u0026gt;Git Push(一键提交)-\u0026gt;Vercel会自动获取GitHub上的更改-\u0026gt; Vercel自动编译-\u0026gt;网站自动刷新。\n","permalink":"https://imrcao.top/posts/0.2%E4%BB%8E%E9%9B%B6%E6%90%AD%E5%BB%BA%E5%85%A8%E8%87%AA%E5%8A%A8%E5%8C%96%E6%9E%81%E5%AE%A2%E5%8D%9A%E5%AE%A2/","summary":"一、引言：为什么选择这套工作流？ 痛点分析：\n我之前使用的是wordpress + WampServer + 阿里云创建的网站，这个流程最大的缺点就是在编辑笔记时需要去wordpress后台，后台不仅慢，编辑起来还费劲，虽然也可以在Obsidian中配合wordpress插件，但总还是感觉wordpress臃肿，不仅如此，服务器使用的是轻量级的，只有2M的带宽，访问很慢，而且域名备案流程繁琐且缓慢。接下来我介绍的流程可以免费托管，只需要为域名付费即可，而且域名无需在国内备案。\n我的终极解法：\n一套本地丝滑写作、云端自动构建、且几乎零成本的现代化技术博客方案。\n核心技术栈预览：\nObsidian (本地编辑) + PicGo (图床管理) + Hugo (静态生成) + GitHub (版本控制) + Vercel (全球部署)。\n二、 构建本地终极写作环境 (Obsidian 篇) 1. 认识 Obsidian\n为什么它是技术笔记的神器（Markdown 原生、双链、极速响应），在遇到Obsidian之前我使用的是有道云和xmind，随着笔记内容越来越多有道云启动都要半天，现在还有好多广告。Obsidian完全本地化管理，数据安全，社区插件生态丰富，可以创建白板，一定程度上可以取代xmind。下载链接：https://obsidian.md/zh/\n2. 图文分离：优雅的图片管理方案 (PicGo)\n（1）、本地图片的痛点（分享不便、打包体积大）。技术笔记里面通常有很多图片，当积累一定规模时，无论时分享还是传到Github上进行云端备份都是一个棘手的问题。如果将笔记部署到网站里，那么图片多的时候往往加载很慢，对服务器带宽是一个极大的考验。所以图文分离是一个很有必要的步骤。\n（2）、配置阿里云OSS对象存储。关于OSS不细述，只讲几个关键点。首先是关于AccessKey的，点击头像找到“权限与安全-\u0026gt;AccessKey”，我这里使用RAM用户AccessKey，如图1所示。然后根据配置执行生成即可，保存好你的AccessID和Key，只显示一次。然后进入对象存储OSS控制页面，先创建一个bucket，点击创建的bucket，找到权限控制-\u0026gt;阻止公共访问，点击关闭，如图2所示。在同一个页面，找到读写权限，将“Bucket ACL\u0026quot;设置为公共读。不然当你将图片链接放入笔记中时并不会正常显示图片。OSS有桌面版可视化管理工具，也可以下载使用。\n（3）、配置 PicGo 图床方案（搭配阿里云 OSS 云存储）。首先安装PicGo，Github下载链接：https://github.com/Molunerfinn/PicGo/releases。我下载的是2.5.2版本。如图2-2-2-1所示，我使用的是阿里云的OSS作为图床，填入相关配置后，保存即可。关于\n（4）、在 Obsidian 中实现“复制粘贴，自动上传并返回图床链接”的丝滑体验。这一步可以实现在Obsidian笔记中直接粘贴图片（配合Snipaste截图软件更方便，F1直接截图，Ctrl + V直接复制），自动将图片传到图床，并返回图床的链接。在Obsidian中下载一个叫“Image auto upload”的第三方插件，启用插件后，进入插件设置，设置PicGOServer 上传接口：http://127.0.0.1:36677/upload，一般默认就是这个值，端口号和PicGo中设置的保持一致，如下图所示。\n3. 云端托管与版本控制 (Obsidian Git)\n安装与配置 Obsidian Git 插件。\n（1）、即使不准备搭建个人博客，只要你有记录笔记的习惯，这一部分对你也有很多帮助。Obsidian自带有云端同步功能，而且是端到端加密的，但是需要付费。另一个方案就是将Obsidian仓库上传到Github，这样可以实现在不同的工作地点对同一篇笔记进行无缝编辑。 （2）、首先创建一个Github仓库，然后将仓库克隆到本地，这一步可以参考笔记“Github配合Sourcetree管理UE4工程”（牵扯到SSH密钥）。在Obsidian中点击仓库管理，选择打开本地仓库，将刚才克隆的本地到仓库作为Obsidian的工作空间。在.gitignore文件中可以设置忽略的文件。\n（3）、在Obsidian中下载Git第三方插件，启用后会在右侧出现Git操作面板，如图所示。常用的流程如下，当修改文件后，点击加号（Stage All）暂存所有，然后在输入框（默认Update Bolg：）输入当前修改的内容，接着点击对号（Commit）提交，最后点击上传按钮（push）推送到GitHub仓库。如果是比较小的修改，可以直接点击第一个向上的箭头（Commit-And-Sync）一键提交和推送。\n总结\n到这里已经实现了Obsidian + Picgo + Github的笔记托管方案。如果不考虑创建个人博客的话，这套流程用来写笔记也是非常舒服的，数据同步，免费托管，图文分离。","title":"从零搭建全自动化极客博客 (Obsidian + Hugo + Vercel)"},{"content":"关于我的内容\u0026hellip;..\n","permalink":"https://imrcao.top/about/","summary":"关于我的内容\u0026hellip;..","title":"about"},{"content":"问题 用PakTools插件打包出来的pak文件，打包运行挂载pak文件后（使用PakLoader插件加载），有的pak文件系统无法加载指定路径的资产。\n原因分析： 使用cmd命令查看pak文件，得到如下输出：\n1 2 3 4 5 6 cmd：UnrealPak.exe F:\\00_Imrcao\\01_MyProject\\PreviewTools\\Saved\\DownloadedPaks\\Models\\chair_P.pak -List Mount point ../../../PreviewTool/ LogPakFile: Display: \u0026#34;AssetRegistry.bin\u0026#34; offset: 0, size: 3948 bytes, LogPakFile: Display: \u0026#34;Content/Assets/Materials/Models/Fabric_3.uasset\u0026#34; LogPakFile: Display: \u0026#34;Content/Assets/Materials/Models/Fabric_3.uexp\u0026#34; LogPakFile: Display: \u0026#34;Content/Assets/Materials/Models/Metal_2.uasset\u0026#34; 1 2 3 4 5 6 7 cmd：UnrealPak.exe F:\\00_Imrcao\\01_MyProject\\PreviewTools\\Saved\\DownloadedPaks\\Models\\Bed_P.pak -List Mount point ../../../ PreviewTool/Content/Assets/Texture/UMG/Thumbnails/Models/Bed.ubulk\u0026#34; LogPakFile: Display: \u0026#34;PreviewTool/Content/Assets/Texture/UMG/Thumbnails/Models/Bed.uexp\u0026#34; LogPakFile: Display: \u0026#34;PreviewTool/Content/Blueprints/Sku/Models/Bed.uasset\u0026#34; LogPakFile: Display: \u0026#34;PreviewTool/Content/Blueprints/Sku/Models/Bed.uexp\u0026#34; 可以发现两个pak文件的mountPoint不一样，默认mountPoint系统自己计算，但也可以自己设置。那么mountPoint不一致会有什么问题呢？先看我的挂载代码。\n1 UPakLoaderLibrary::MountPakFile(PakPaths, \u0026#34;\u0026#34;) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 //挂载pak UPakLoaderLibrary::MountPakFile(PakPaths, \u0026#34;\u0026#34;) //注册挂载点，这个是核心 UPakLoaderLibrary::RegisterMountPoint(\u0026#34;/PTPakDLC/\u0026#34;, \u0026#34;../../../PreviewTool/Content/\u0026#34;); // UPakLoaderLibrary::LoadPakAssetRegistryFile(\u0026#34;../../../PreviewTool/AssetRegistry.bin\u0026#34;); //拼接资产名称 TArray\u0026lt;FString\u0026gt;pakFiles = UPakLoaderLibrary::GetFilesInPak(PakPaths, true); for (auto pakFile : pakFiles) { FString str = UBlueprintPathsLibrary::GetExtension(pakFile); if (!str.Equals(\u0026#34;uexp\u0026#34;) \u0026amp;\u0026amp; !str.Equals(\u0026#34;ubulk\u0026#34;)) { //仅加载BPActor if (pakFile.Contains(\u0026#34;Blueprints/Sku\u0026#34;)) { pakFile.Split(\u0026#34;Content/\u0026#34;, nullptr, \u0026amp;pakFile); pakFile = \u0026#34;/PTPakDLC/\u0026#34; + pakFile; //去掉.uasset后缀，获取纯包路径:/PTPakDLC/Blueprints/Sku/Lights/SpotLight_P pakFile.Split(\u0026#34;.\u0026#34;, \u0026amp;pakFile, nullptr); AssetFiles.AddUnique(pakFile); } } } //根据资产名称加载pak中的资产 UPakLoaderLibrary::GetPakFileClass(const FString \u0026amp;Filename) 问题在于最后需要的资产的路径：/PTPakDLC/Blueprints/Sku/Lights/SpotLight_P，需要长这样。但是我之前是这样拼接的：pakFile.Replace(TEXT(\u0026ldquo;PreviewTool/Content\u0026rdquo;), TEXT(\u0026quot;/PTPakDLC/\u0026quot;));我以为所有的pak文件都像Bed_P.pak文件一样，内部资产路径都是以PreviewTool/Content开头的。但是chair_P.pak是以Content开头的。但实际上它们俩在引擎中的实际资产路径都是以../../../PreviewTool/Content开头，由于注册了挂载点：RegisterMountPoint(\u0026quot;/PTPakDLC/\u0026quot;, \u0026ldquo;../../../PreviewTool/Content/\u0026quot;)，引擎就会把所有的\u0026rdquo;../../../PreviewTool/Content/\u0026ldquo;替换为\u0026rdquo;/PTPakDLC/\u0026quot;，但又由于UPakLoaderLibrary::GetFilesInPak(PakPaths, true)，返回的是PreviewTool/Content/\u0026hellip;/\u0026hellip;或者是Content/\u0026hellip;/\u0026hellip;。所以使用pakFile.Replace就不妥当，要想获取这样的路径：/PTPakDLC/Blueprints/Sku/\u0026hellip;/\u0026hellip;/，就必须改变方法，最终目的就是要让引擎能够识别/PTPakDLC/Blueprints/Sku/\u0026hellip;/\u0026hellip;/路径，并且在pak中确实有这个资产。\n总结 刚开始并不知道是这个原因导致的pak资产加载失败，尝试了很多方法，网上说加载了这个资产UPakLoaderLibrary::LoadPakAssetRegistryFile (\u0026quot;../../../PreviewTool/AssetRegistry.bin\u0026quot;)，引擎可以自动识别资产路径，还没有深入测试。\n","permalink":"https://imrcao.top/posts/0.1%E4%BD%BF%E7%94%A8ue%E6%8C%82%E8%BD%BDpak%E6%97%B6%E9%81%87%E5%88%B0%E7%9A%84%E4%B8%80%E4%B8%AA%E9%97%AE%E9%A2%98/","summary":"问题 用PakTools插件打包出来的pak文件，打包运行挂载pak文件后（使用PakLoader插件加载），有的pak文件系统无法加载指定路径的资产。\n原因分析： 使用cmd命令查看pak文件，得到如下输出：\n1 2 3 4 5 6 cmd：UnrealPak.exe F:\\00_Imrcao\\01_MyProject\\PreviewTools\\Saved\\DownloadedPaks\\Models\\chair_P.pak -List Mount point ../../../PreviewTool/ LogPakFile: Display: \u0026#34;AssetRegistry.bin\u0026#34; offset: 0, size: 3948 bytes, LogPakFile: Display: \u0026#34;Content/Assets/Materials/Models/Fabric_3.uasset\u0026#34; LogPakFile: Display: \u0026#34;Content/Assets/Materials/Models/Fabric_3.uexp\u0026#34; LogPakFile: Display: \u0026#34;Content/Assets/Materials/Models/Metal_2.uasset\u0026#34; 1 2 3 4 5 6 7 cmd：UnrealPak.exe F:\\00_Imrcao\\01_MyProject\\PreviewTools\\Saved\\DownloadedPaks\\Models\\Bed_P.pak -List Mount point ../../../ PreviewTool/Content/Assets/Texture/UMG/Thumbnails/Models/Bed.ubulk\u0026#34; LogPakFile: Display: \u0026#34;PreviewTool/Content/Assets/Texture/UMG/Thumbnails/Models/Bed.uexp\u0026#34; LogPakFile: Display: \u0026#34;PreviewTool/Content/Blueprints/Sku/Models/Bed.uasset\u0026#34; LogPakFile: Display: \u0026#34;PreviewTool/Content/Blueprints/Sku/Models/Bed.uexp\u0026#34; 可以发现两个pak文件的mountPoint不一样，默认mountPoint系统自己计算，但也可以自己设置。那么mountPoint不一致会有什么问题呢？先看我的挂载代码。\n1 UPakLoaderLibrary::MountPakFile(PakPaths, \u0026#34;\u0026#34;) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 //挂载pak UPakLoaderLibrary::MountPakFile(PakPaths, \u0026#34;\u0026#34;) //注册挂载点，这个是核心 UPakLoaderLibrary::RegisterMountPoint(\u0026#34;/PTPakDLC/\u0026#34;, \u0026#34;.","title":"使用UE挂载Pak时遇到的一个问题"},{"content":"一、基本思路 操作封装。将每一个可撤销的用户行为，抽象为一个\u0026quot;命令对象\u0026quot;（IUndoableCommand），该对象下有两个方法：Do()、Undo()； 使用 TArray 存储最近的 5 步操作。创建撤销栈，先进后出。 创建撤销管理器：FUndoManager，管理\u0026quot;命令对象\u0026quot;。 绑定 Ctrl + Z。 二、核心类设计 1、抽象命令接口 IUndoableCommand\n1 2 3 4 5 6 7 8 9 10 #pragma once #include \u0026#34;CoreMinimal.h\u0026#34; class IUndoableCommand { public: virtual ~IUndoableCommand() {} virtual void Do() = 0; virtual void Undo() = 0; }; 2、创建命令对象，比如平移对象：FTranslateActorCommand。（也可以类似实现：FActorPropertyCommand、FDeleteActorCommand 等操作）\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #pragma once #include \u0026#34;CoreMinimal.h\u0026#34; #include \u0026#34;IUndoableCommand.h\u0026#34; #include \u0026#34;GameFramework/Actor.h\u0026#34; class FTranslateActorCommand : public IUndoableCommand { public: FTranslateActorCommand(AActor* InActor, const FTransform\u0026amp; InOldLocation, const FTransform\u0026amp; InNewLocation) : Actor(InActor), OldLocation(InOldLocation), NewLocation(InNewLocation) {} virtual void Do() override; virtual void Undo() override; private: AActor* Actor; FTransform OldLocation; FTransform NewLocation; }; // cpp void FTranslateActorCommand::Do() { } void FTranslateActorCommand::Undo() { if (Actor) { Actor-\u0026gt;SetActorTransform(OldLocation); } } 3、创建撤销管理器：FUndoManager，设计为单例\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 #pragma once #include \u0026#34;CoreMinimal.h\u0026#34; #include \u0026#34;IUndoableCommand.h\u0026#34; class FUndoManager { public: static FUndoManager\u0026amp; Get(); void ExecuteCommand(TSharedPtr\u0026lt;IUndoableCommand\u0026gt; Command); void Undo(); private: // 私有构造，防止外部创建 FUndoManager() = default; TArray\u0026lt;TSharedPtr\u0026lt;IUndoableCommand\u0026gt;\u0026gt; UndoStack; static const uint8 MaxUndoSteps = 5; }; // cpp #include \u0026#34;UndoManager.h\u0026#34; FUndoManager\u0026amp; FUndoManager::Get() { // 局部静态变量实现单例（线程安全、内存自动释放）： static FUndoManager Instrance; return Instrance; } void FUndoManager::ExecuteCommand(TSharedPtr\u0026lt;IUndoableCommand\u0026gt; Command) { if (!Command.IsValid()) return; Command-\u0026gt;Do(); UndoStack.Insert(Command, 0); if (UndoStack.Num() \u0026gt; MaxUndoSteps) { UndoStack.RemoveAt(UndoStack.Num() - 1); } UE_LOG(LogTemp, Display, TEXT(\u0026#34;ExecuteCommandStackNum: %d\u0026#34;), UndoStack.Num()); } void FUndoManager::Undo() { if (UndoStack.Num() \u0026gt; 0) { UE_LOG(LogTemp, Display, TEXT(\u0026#34;UndoStackNum: %d\u0026#34;), UndoStack.Num()); TSharedPtr\u0026lt;IUndoableCommand\u0026gt; Command = UndoStack[0]; Command-\u0026gt;Undo(); UndoStack.RemoveAt(0); } } 三、使用方式 在平移操作完成之后，调用：\n1 2 TSharedPtr\u0026lt;FTranslateActorCommand\u0026gt; Cmd = MakeShared\u0026lt;FTranslateActorCommand\u0026gt;(Actor, OldTransform, Actor-\u0026gt;GetTransform()); FUndoManager::Get().ExecuteCommand(Cmd); 四、绑定 Ctrl + Z 1 2 3 4 5 6 7 PlayerInputComponent-\u0026gt;BindAction(\u0026#34;UndoCommand\u0026#34;, IE_Pressed, this, \u0026amp;ABPT_TransformerPawn::OnUndoPressed); void ABPT_TransformerPawn::OnUndoPressed() { UE_LOG(LogTemp, Display, TEXT(\u0026#34;Ctrl Z\u0026#34;)); FUndoManager::Get().Undo(); } 五、总结 撤销功能的设计是一个非常典型的面向对象设计案例，其中使用了如下知识点：\n封装：将\u0026quot;命令操作\u0026quot;封装成一个类对象 IUndoableCommand，提供了 Do() 和 Undo() 方法。FUndoManager 封装了撤销栈，只提供接口 ExecuteCommand() 和 Undo()。 继承：所有命令类都继承自接口 IUndoableCommand。 多态：可以使用基类指针 TSharedPtr\u0026lt;IUndoableCommand\u0026gt; 存储不同类型的命令对象，比如平移、删除、属性调整等。通过基类指针调用 Do() 和 Undo() 方法，实现运行时多态： 1 2 TSharedPtr\u0026lt;IUndoableCommand\u0026gt; Cmd = MakeShared\u0026lt;FTranslateActorCommand\u0026gt;(...); Cmd-\u0026gt;Undo(); // 实际调用的是 FTranslateActorCommand::Undo() ","permalink":"https://imrcao.top/posts/0.4ctrl-+-z%E6%92%A4%E9%94%80%E5%8A%9F%E8%83%BD%E5%AE%9E%E7%8E%B0%E6%80%9D%E8%B7%AF/","summary":"一、基本思路 操作封装。将每一个可撤销的用户行为，抽象为一个\u0026quot;命令对象\u0026quot;（IUndoableCommand），该对象下有两个方法：Do()、Undo()； 使用 TArray 存储最近的 5 步操作。创建撤销栈，先进后出。 创建撤销管理器：FUndoManager，管理\u0026quot;命令对象\u0026quot;。 绑定 Ctrl + Z。 二、核心类设计 1、抽象命令接口 IUndoableCommand\n1 2 3 4 5 6 7 8 9 10 #pragma once #include \u0026#34;CoreMinimal.h\u0026#34; class IUndoableCommand { public: virtual ~IUndoableCommand() {} virtual void Do() = 0; virtual void Undo() = 0; }; 2、创建命令对象，比如平移对象：FTranslateActorCommand。（也可以类似实现：FActorPropertyCommand、FDeleteActorCommand 等操作）\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #pragma once #include \u0026#34;CoreMinimal.","title":"Ctrl + Z撤销功能实现思路"},{"content":"前言： 第一次使用UE4打包IOS的话会遇到很多坑，这里记录了各种坑的填补方法。其实打包的方法有很多，需要有一台MAC电脑，如果你习惯在MAC上使用UE4，可以直接在MAC上进行打包。也可以在PC端进行远程连接打包。我这里只记录在PC上进行远程打包。我将这篇笔记分为四部分：MAC上创建证书相关；PC苹果开发者网站中证书及文件操作相关部分；PC主机和MAC机连接部分；UE4配置及打包部分。我测试的环境：一台MAC一体机，一台PC主机，一台IPADPro，使用UE427创建的C++工程，具有苹果开发者账号（不讲账号购买部分）。\n一、MAC上创建开发者证书 在MAC上，找到“钥匙串”程序，打开后在电脑的上左上方的菜单栏中找到钥匙串-\u0026gt;证书助理-\u0026gt;从开发机构申请颁布证书。打开后输入邮箱和名称，在下方勾选“存储到磁盘”和“让我指定密钥对信息”，如图1-1所示。点击“继续”选择存储位置，在“密钥对信息”页面使用默认值就行，点击“继续”会创建一个后缀为“.certSigningRequest”的证书，如图1-2所示。证书创建好后拷贝到PC主机上（因为苹果开发者账号是从PC主机上登录的），在MAC上登录就不用拷贝了。\n图1-1\n图1-2\n二、登录苹果开发者账号 1、进入网站“https://idmsa.apple.com/”。登录进去后首先开始创建证书，如图2-1-1所示。我这里选择“IOS App Developmant”，如图2-2-2所示。下一步选择从MAC上创建的后缀为“.certSigningRequest”的开发者证书，如图2-2-3所示。下一步点击“Download”将配置好的证书下载到本地，如图2-4所示。最终会得到一个“.cer”格式的文件。\n图2-1-1\n图2-1-2\n图2-1-3\n图2-1-4\n2、第二步添加身份标识，创建AppID。如图2-2-1所示。选择App IDs，如图2-2-2所示，下一步选择“APP”，点击继续，如图２-2-3所示。首先填写描述，根据项目填写就行，接着填写“BundleID”，根据提示填写，我这里写的是“com.imrcao.ledv3.app”,要将BundleID记住，后面要在UE4的项目设置中用的。如图2-2-4所示。点击继续后就可以点击注册了，如图2-2-5所示。\n图2-2-1\n图2-2-2\n图2-2-3\n图2-2-4\n图2-2-5\n3、接着要添加设备，这里要获取IPAD的UDID，获取方法可以在网上搜一下，可以使用“蒲公英”，也可以使用爱思助手（个人比较推荐这个），使用爱思助手还可以安装自己的IPA程序。点击添加按钮，如图２-3-1所示。输入名字和UDID后点击继续即可，如图２-3-2所示。 图2-3-1\n图2-3-2\n4、第四步创建配置文件，这个文件是要在UE4中使用的。点击添加按钮，如图2-4-1所示。在新的一页选择“ios App Developmant”，点击继续如图2-4-2所示。选择第二步创建的AppID，点击继续，如图2-4-3所示。接着选择第一步创建的证书，如图2-4-4所示。点击继续后选择第三步创建的设备ID，如图2-4-5所示。最后预览信息无误后点击生成按钮，生成配置文件。如图2-4-6所示。配置好后回到“Profiles”列表，点击“Download”下载配置文件，会得到一个后缀为“.mobileprovision”文件。这个文件后面会直接导入UE4项目设置中使用。\n图2-4-1\n图2-4-2\n图2-4-3\n图2-4-4\n图2-4-5\n图2-4-6\n三、MAC上认证“.cer”安全证书 1、第二部分完成后PC本地应该有后缀分别为“.cer”和“.mobileprovision”两个文件，如图3-1-1所示。接下来要将“.cer”文件拷贝到MAC上，认证证书，然后生成\u0026quot;.p12\u0026quot;信息交换文件。 图3-1-1\n2、在MAC上双击“.cer”文件，在钥匙串的登录页就可以看到证书，注意在窗口上方一栏中会显示“此证书有效”的图标，如图3-2-1所示。如果证书是无效的话，应该是缺少两个系统证书，如图3-2-2所示，一个23年过期，一个30年过期。在证书有效的情况下对刚才导入的\u0026quot;.cer\u0026quot;文件单击右键选择导出（注意不是在“专用密钥”那一栏单击，在这里单击导出我没有试过）。在弹出的窗口点击“存储”，如图3-2-3所示。然后输入密码即可。这时会生成一个\u0026quot;.p12\u0026quot;文件。将这个文件传输到PC上，后面在UE4项目设置中直接导入。\n图3-2-1\n图3-2-2\n图3-2-3\n3、在PC上最终我们会使用这个两个文件，分别是后缀“.mobileprovision”和“.p12”文件。如图3-3-1所示。 图3-3-1\n四、连接PC主机和MAC 1、如果你创建的是UE4的C++工程，那么打包IOS时就需要使用XCode编译，由于我们是在PC上打包，PC上是没有XCode的，所以要连接MAC机。使用无线和有线连接都可以，最终要使两台机器处于同一个局域网内，且可以“Ping”通。连接好后，在MAC上打开“共享”窗口，勾选“远程登录”，选择“所有用户”。如图4-1-1所示。\n图4-1-1\n2、查看MAC的用户名：在MAC中找到“终端”程序，终端窗口上面的名字就是用户名，这个在UE4项目设置中会用到。如图4-2-1所示。 图4-2-1\n五、在PC上使用UE４打包。 1、在UE４工程的项目设置中找到“Platforms-\u0026gt;iOS”，点击“Import Provision”选择后缀“.mobileprovision”文件并导入，点击“Import Certificate”选择后缀“.p12”文件，输入密码后导入。在“Status”下显示“Valid”说明证书有效。有时候证书和密钥很多，如果前面的复选框都不勾选的话就使用默认的移动证书和密钥，勾选的话就使用指定证书和密钥。如图5-1-1所示。 2、在下方的“Bundle Information”一栏需要填写证书相关的信息，这里就会用到在开发者网站中创建AppID时填写的Bundle ID。Bundle Display Name是APP的显示名称，可以随便填写。Bundle Name也可以自己填写。Bundle Identifier一栏填写在开发者网站中创建AppID时填写的Bundle ID。如图5-1-1所示。 图5-1-1\n3、接着要生成SSH Key，在项目设置“Platforms - ios-Build”下，展开“Remote Build Options”。 Remote Server Name 填写Mac的IP地址，RSync User Name 填写MAC的用户名（可以使用终端程序查看），接着就可以点击“Generate SSH Key”按钮了。 接下来会弹出一个命令行窗口。 （1）、首先随便按一个键继续； （2）、接下来会提示你输入MAC用户对应的登录密码，输入后按回车（这里输入是不可见的，盲打就行）； （3）、Enter same passphrase again:什么都不要输入，直接回车； （5）、接着会出来一堆代码，不要管直接按回车继续； （6）、接着再次输入MAC用户的登录密码，回车继续； 如果没有提示什么错误的话，说明Remote Build就设置成功过了，过一会SSH Key文件就生成在本地了。如图5-3-1所示。\n图5-3-1\n4、接着点击“File -\u0026gt;Package Project-\u0026gt;IOS”打包，打包成功后会生成一个“.ipa”文件。如图5-4-1\n图5-4-1\n六、安装 1、将IPAD使用数据线和PC连接，下载打开爱思助手，选择应用游戏-\u0026gt;导入安装，选择文件即可安装。如图6-1-1所示。 ","permalink":"https://imrcao.top/posts/%E5%BD%92%E6%A1%A3/ue4%E6%89%93%E5%8C%85ios%E8%AF%A6%E7%BB%86%E7%AC%94%E8%AE%B0/","summary":"前言： 第一次使用UE4打包IOS的话会遇到很多坑，这里记录了各种坑的填补方法。其实打包的方法有很多，需要有一台MAC电脑，如果你习惯在MAC上使用UE4，可以直接在MAC上进行打包。也可以在PC端进行远程连接打包。我这里只记录在PC上进行远程打包。我将这篇笔记分为四部分：MAC上创建证书相关；PC苹果开发者网站中证书及文件操作相关部分；PC主机和MAC机连接部分；UE4配置及打包部分。我测试的环境：一台MAC一体机，一台PC主机，一台IPADPro，使用UE427创建的C++工程，具有苹果开发者账号（不讲账号购买部分）。\n一、MAC上创建开发者证书 在MAC上，找到“钥匙串”程序，打开后在电脑的上左上方的菜单栏中找到钥匙串-\u0026gt;证书助理-\u0026gt;从开发机构申请颁布证书。打开后输入邮箱和名称，在下方勾选“存储到磁盘”和“让我指定密钥对信息”，如图1-1所示。点击“继续”选择存储位置，在“密钥对信息”页面使用默认值就行，点击“继续”会创建一个后缀为“.certSigningRequest”的证书，如图1-2所示。证书创建好后拷贝到PC主机上（因为苹果开发者账号是从PC主机上登录的），在MAC上登录就不用拷贝了。\n图1-1\n图1-2\n二、登录苹果开发者账号 1、进入网站“https://idmsa.apple.com/”。登录进去后首先开始创建证书，如图2-1-1所示。我这里选择“IOS App Developmant”，如图2-2-2所示。下一步选择从MAC上创建的后缀为“.certSigningRequest”的开发者证书，如图2-2-3所示。下一步点击“Download”将配置好的证书下载到本地，如图2-4所示。最终会得到一个“.cer”格式的文件。\n图2-1-1\n图2-1-2\n图2-1-3\n图2-1-4\n2、第二步添加身份标识，创建AppID。如图2-2-1所示。选择App IDs，如图2-2-2所示，下一步选择“APP”，点击继续，如图２-2-3所示。首先填写描述，根据项目填写就行，接着填写“BundleID”，根据提示填写，我这里写的是“com.imrcao.ledv3.app”,要将BundleID记住，后面要在UE4的项目设置中用的。如图2-2-4所示。点击继续后就可以点击注册了，如图2-2-5所示。\n图2-2-1\n图2-2-2\n图2-2-3\n图2-2-4\n图2-2-5\n3、接着要添加设备，这里要获取IPAD的UDID，获取方法可以在网上搜一下，可以使用“蒲公英”，也可以使用爱思助手（个人比较推荐这个），使用爱思助手还可以安装自己的IPA程序。点击添加按钮，如图２-3-1所示。输入名字和UDID后点击继续即可，如图２-3-2所示。 图2-3-1\n图2-3-2\n4、第四步创建配置文件，这个文件是要在UE4中使用的。点击添加按钮，如图2-4-1所示。在新的一页选择“ios App Developmant”，点击继续如图2-4-2所示。选择第二步创建的AppID，点击继续，如图2-4-3所示。接着选择第一步创建的证书，如图2-4-4所示。点击继续后选择第三步创建的设备ID，如图2-4-5所示。最后预览信息无误后点击生成按钮，生成配置文件。如图2-4-6所示。配置好后回到“Profiles”列表，点击“Download”下载配置文件，会得到一个后缀为“.mobileprovision”文件。这个文件后面会直接导入UE4项目设置中使用。\n图2-4-1\n图2-4-2\n图2-4-3\n图2-4-4\n图2-4-5\n图2-4-6\n三、MAC上认证“.cer”安全证书 1、第二部分完成后PC本地应该有后缀分别为“.cer”和“.mobileprovision”两个文件，如图3-1-1所示。接下来要将“.cer”文件拷贝到MAC上，认证证书，然后生成\u0026quot;.p12\u0026quot;信息交换文件。 图3-1-1\n2、在MAC上双击“.cer”文件，在钥匙串的登录页就可以看到证书，注意在窗口上方一栏中会显示“此证书有效”的图标，如图3-2-1所示。如果证书是无效的话，应该是缺少两个系统证书，如图3-2-2所示，一个23年过期，一个30年过期。在证书有效的情况下对刚才导入的\u0026quot;.cer\u0026quot;文件单击右键选择导出（注意不是在“专用密钥”那一栏单击，在这里单击导出我没有试过）。在弹出的窗口点击“存储”，如图3-2-3所示。然后输入密码即可。这时会生成一个\u0026quot;.p12\u0026quot;文件。将这个文件传输到PC上，后面在UE4项目设置中直接导入。\n图3-2-1\n图3-2-2\n图3-2-3\n3、在PC上最终我们会使用这个两个文件，分别是后缀“.mobileprovision”和“.p12”文件。如图3-3-1所示。 图3-3-1\n四、连接PC主机和MAC 1、如果你创建的是UE4的C++工程，那么打包IOS时就需要使用XCode编译，由于我们是在PC上打包，PC上是没有XCode的，所以要连接MAC机。使用无线和有线连接都可以，最终要使两台机器处于同一个局域网内，且可以“Ping”通。连接好后，在MAC上打开“共享”窗口，勾选“远程登录”，选择“所有用户”。如图4-1-1所示。\n图4-1-1\n2、查看MAC的用户名：在MAC中找到“终端”程序，终端窗口上面的名字就是用户名，这个在UE4项目设置中会用到。如图4-2-1所示。 图4-2-1\n五、在PC上使用UE４打包。 1、在UE４工程的项目设置中找到“Platforms-\u0026gt;iOS”，点击“Import Provision”选择后缀“.mobileprovision”文件并导入，点击“Import Certificate”选择后缀“.p12”文件，输入密码后导入。在“Status”下显示“Valid”说明证书有效。有时候证书和密钥很多，如果前面的复选框都不勾选的话就使用默认的移动证书和密钥，勾选的话就使用指定证书和密钥。如图5-1-1所示。 2、在下方的“Bundle Information”一栏需要填写证书相关的信息，这里就会用到在开发者网站中创建AppID时填写的Bundle ID。Bundle Display Name是APP的显示名称，可以随便填写。Bundle Name也可以自己填写。Bundle Identifier一栏填写在开发者网站中创建AppID时填写的Bundle ID。如图5-1-1所示。 图5-1-1\n3、接着要生成SSH Key，在项目设置“Platforms - ios-Build”下，展开“Remote Build Options”。 Remote Server Name 填写Mac的IP地址，RSync User Name 填写MAC的用户名（可以使用终端程序查看），接着就可以点击“Generate SSH Key”按钮了。 接下来会弹出一个命令行窗口。 （1）、首先随便按一个键继续； （2）、接下来会提示你输入MAC用户对应的登录密码，输入后按回车（这里输入是不可见的，盲打就行）； （3）、Enter same passphrase again:什么都不要输入，直接回车； （5）、接着会出来一堆代码，不要管直接按回车继续； （6）、接着再次输入MAC用户的登录密码，回车继续； 如果没有提示什么错误的话，说明Remote Build就设置成功过了，过一会SSH Key文件就生成在本地了。如图5-3-1所示。","title":"UE4打包IOS详细笔记"},{"content":"在 C++ 编程中，#pragma optimize(\u0026quot;\u0026quot;, off) 和 #pragma optimize(\u0026quot;\u0026quot;, on) 是用于控制特定代码段的编译器优化设置的预处理指令。这些指令可以帮助在调试特定代码段时禁用优化，以便更容易地跟踪和调试问题。 在 Unreal Engine 4 的开发中，可以使用这些指令来确保某些代码段在编译时不被优化，通常用于调试复杂逻辑或在执行关键任务时确保代码的行为与预期一致。\n","permalink":"https://imrcao.top/posts/%E5%BD%92%E6%A1%A3/c++%E7%BC%96%E7%A8%8B-%E7%A6%81%E7%94%A8%E4%BC%98%E5%8C%96/","summary":"在 C++ 编程中，#pragma optimize(\u0026quot;\u0026quot;, off) 和 #pragma optimize(\u0026quot;\u0026quot;, on) 是用于控制特定代码段的编译器优化设置的预处理指令。这些指令可以帮助在调试特定代码段时禁用优化，以便更容易地跟踪和调试问题。 在 Unreal Engine 4 的开发中，可以使用这些指令来确保某些代码段在编译时不被优化，通常用于调试复杂逻辑或在执行关键任务时确保代码的行为与预期一致。","title":"C++编程-禁用优化"},{"content":"前言 在本指南中你将了解IKRig和IKRetargeter的使用方法；将使用了不同的骨架网格体的动画序列进行重定向，使动画序列具有通用性。使用了Epic商城中的AnimStarterPack和UnrealLearningKitGames；使用引擎版本：Unreal Engine5.1；目的：将AnimStarterPack中的动画序列重定向到UnrealLearningKitGames项目中的卡通人物。 IK Rig创建 在Content文件夹中单机右键找到：Animation-\u0026gt;IK Rig即可创建这两个资产。注意创建IK Rig时需要选择一个Skeletal Mesh。 需要创建两个IK Rig，分别选择Skeletal Mesh：SK_Mannequin And SK_EpicCharacter，分别命名为：Mannequin_IKRig And LearningKitCharacter_IKRig。如下图所示。 IK Rig重定向根骨骼 需要分别对两个IK Rig进行重定向根骨骼 重定向根骨骼：一般选择胯部的骨骼，例如pelvis, Hip。选中骨骼单机右键，选择SetRetargetRoot。如下图所示。 IK Rig重定向链 什么是链 起始骨骼指向性单一，中间不要有分支，如下图所示：左侧为指向性单一的骨骼，可以形成一条链，右侧将会形成两条链。 创建骨骼链。 选中指定骨骼，单机右键选择“New Retarget Chain from Selected Bones”，重命名后选择“No Goal”。 需要注意的是在为大腿创建骨骼时有一个旋转骨骼“calf_twist_01_I\u0026quot;不要包含进去，如下图所示。如果你想重定向这个骨骼的话需要单独创建一个链。 最后创建完成后的骨骼链列表，如下图所示。 IKRetargeter创建 在Content空白处单机右键找到：Animation-\u0026gt;IK Rig。创建一个Retargeter，需要选择一个IKRig，因为我们需要将AnimStarterPack中的动画序列重定向给UnrealLearningKitGames中的Sk_EpicCharacter。所以这里选择EpicCharacter_IKRig。命名为：Mannequin_Retargeter.如下图所示。 选择目标：LearningKitCharacter_IKRig。 IKRetarget面板 1：管理目标和源的姿势，点击”Edit Mode“可以编辑保存创建姿势，最常用的是T-Pos和A-Pos。如果目标和源的姿势不一样，需要调整其中一个骨骼的姿态尽量和另外一个相似，相似度越高重定向出来的动画越准确。 2：源和目标的骨骼列表。 3：预览视图。 4：细节面板，在这里可以调整Mesh的偏移值，初始两个Mesh有可能重叠。通过调整偏移值使两个Mesh分开。 5：骨骼链列表和动画序列列表。通过骨骼链列表可以查看骨骼链的映射情况，也可手动更改。通过点击动画序列可以预览重定向后的动画。 调整两个Skeletal mesh的姿态，使其最大程度重叠，是重定向动画成功的关键。进入编辑模式后通过调整指定骨骼的旋转等值，如下图所示。 调整到你认为满意的时候，选中需要重定向的动画序列，点击”Export Selected Animations\u0026quot;导出动画序列。 ","permalink":"https://imrcao.top/posts/%E5%BD%92%E6%A1%A3/ik-rig%E9%87%8D%E5%AE%9A%E5%90%91/","summary":"前言 在本指南中你将了解IKRig和IKRetargeter的使用方法；将使用了不同的骨架网格体的动画序列进行重定向，使动画序列具有通用性。使用了Epic商城中的AnimStarterPack和UnrealLearningKitGames；使用引擎版本：Unreal Engine5.1；目的：将AnimStarterPack中的动画序列重定向到UnrealLearningKitGames项目中的卡通人物。 IK Rig创建 在Content文件夹中单机右键找到：Animation-\u0026gt;IK Rig即可创建这两个资产。注意创建IK Rig时需要选择一个Skeletal Mesh。 需要创建两个IK Rig，分别选择Skeletal Mesh：SK_Mannequin And SK_EpicCharacter，分别命名为：Mannequin_IKRig And LearningKitCharacter_IKRig。如下图所示。 IK Rig重定向根骨骼 需要分别对两个IK Rig进行重定向根骨骼 重定向根骨骼：一般选择胯部的骨骼，例如pelvis, Hip。选中骨骼单机右键，选择SetRetargetRoot。如下图所示。 IK Rig重定向链 什么是链 起始骨骼指向性单一，中间不要有分支，如下图所示：左侧为指向性单一的骨骼，可以形成一条链，右侧将会形成两条链。 创建骨骼链。 选中指定骨骼，单机右键选择“New Retarget Chain from Selected Bones”，重命名后选择“No Goal”。 需要注意的是在为大腿创建骨骼时有一个旋转骨骼“calf_twist_01_I\u0026quot;不要包含进去，如下图所示。如果你想重定向这个骨骼的话需要单独创建一个链。 最后创建完成后的骨骼链列表，如下图所示。 IKRetargeter创建 在Content空白处单机右键找到：Animation-\u0026gt;IK Rig。创建一个Retargeter，需要选择一个IKRig，因为我们需要将AnimStarterPack中的动画序列重定向给UnrealLearningKitGames中的Sk_EpicCharacter。所以这里选择EpicCharacter_IKRig。命名为：Mannequin_Retargeter.如下图所示。 选择目标：LearningKitCharacter_IKRig。 IKRetarget面板 1：管理目标和源的姿势，点击”Edit Mode“可以编辑保存创建姿势，最常用的是T-Pos和A-Pos。如果目标和源的姿势不一样，需要调整其中一个骨骼的姿态尽量和另外一个相似，相似度越高重定向出来的动画越准确。 2：源和目标的骨骼列表。 3：预览视图。 4：细节面板，在这里可以调整Mesh的偏移值，初始两个Mesh有可能重叠。通过调整偏移值使两个Mesh分开。 5：骨骼链列表和动画序列列表。通过骨骼链列表可以查看骨骼链的映射情况，也可手动更改。通过点击动画序列可以预览重定向后的动画。 调整两个Skeletal mesh的姿态，使其最大程度重叠，是重定向动画成功的关键。进入编辑模式后通过调整指定骨骼的旋转等值，如下图所示。 调整到你认为满意的时候，选中需要重定向的动画序列，点击”Export Selected Animations\u0026quot;导出动画序列。 ","title":"IK Rig重定向"},{"content":"本节内容 这一节主要讲述在多人游戏中，Gemaplay框架相关各个类是如何处理每个游戏会话中的各种事务的。 GameMode 在多人网络游戏中，GameMode只存在于服务器中。 GameMode可以用来设置游戏中的各个默认类，比如GameState、Default Pawn、HUD、PlayerController等等。除了设置默认类外还有其它丰富的功能，负责游戏规则，比如确定玩家淘汰后会发生什么，以及处理玩家的重生；游戏模式中还可以运行计时器，以便管理比赛的时间。 GameState 在多人网络游戏中，GameState存在于服务器和所有客户端中。由于GameMode只存在于服务器中，那么GameMode中的数据如何传输到客户端呢，GameState可以处理这件事情，它可以将服务器中的数据复制到客户端。GameState更多用于存储游戏状态，比如存储游戏中所有玩家的得分、各个队伍的排名等。 PlayerState 和GameState类似，它也只存在于服务器和所有客户端中。PlayerState只负责处理单个玩家的状态，比如当前玩家的得分、击败数量、身上携带的弹药、属于哪支队伍等。 PlayerController PlayerController存在于服务器和当前本地客户端中。它负责拥有并控制一个Pawn，也可以访问HUD，因此一旦我们有了健康条、得分、击败次数和弹药数量等信息，我们就可以通过PlayerController访问HUD来更新这些信息。 Pawn Pawn其实也就是玩家角色，它存在于服务器和所有客户端中。 HUD 只存在于本地客户端中，HUD负责各种屏幕信息显示，生命条、击败次数、弹药数量等。\n总结 在编写游戏时，我们要时刻考虑哪些逻辑应该写在哪个地方，充分利用引擎游戏框架内置好的功能函数才能使编程事半功倍。\n","permalink":"https://imrcao.top/posts/%E5%BD%92%E6%A1%A3/gameplay%E6%B8%B8%E6%88%8F%E6%A1%86%E6%9E%B6/","summary":"本节内容 这一节主要讲述在多人游戏中，Gemaplay框架相关各个类是如何处理每个游戏会话中的各种事务的。 GameMode 在多人网络游戏中，GameMode只存在于服务器中。 GameMode可以用来设置游戏中的各个默认类，比如GameState、Default Pawn、HUD、PlayerController等等。除了设置默认类外还有其它丰富的功能，负责游戏规则，比如确定玩家淘汰后会发生什么，以及处理玩家的重生；游戏模式中还可以运行计时器，以便管理比赛的时间。 GameState 在多人网络游戏中，GameState存在于服务器和所有客户端中。由于GameMode只存在于服务器中，那么GameMode中的数据如何传输到客户端呢，GameState可以处理这件事情，它可以将服务器中的数据复制到客户端。GameState更多用于存储游戏状态，比如存储游戏中所有玩家的得分、各个队伍的排名等。 PlayerState 和GameState类似，它也只存在于服务器和所有客户端中。PlayerState只负责处理单个玩家的状态，比如当前玩家的得分、击败数量、身上携带的弹药、属于哪支队伍等。 PlayerController PlayerController存在于服务器和当前本地客户端中。它负责拥有并控制一个Pawn，也可以访问HUD，因此一旦我们有了健康条、得分、击败次数和弹药数量等信息，我们就可以通过PlayerController访问HUD来更新这些信息。 Pawn Pawn其实也就是玩家角色，它存在于服务器和所有客户端中。 HUD 只存在于本地客户端中，HUD负责各种屏幕信息显示，生命条、击败次数、弹药数量等。\n总结 在编写游戏时，我们要时刻考虑哪些逻辑应该写在哪个地方，充分利用引擎游戏框架内置好的功能函数才能使编程事半功倍。","title":"Gameplay游戏框架"},{"content":"前言 前言：《用户手册》将向用户介绍TAVP虚拟摄影棚数字资产库终端系统（以下简称TAVP客户端）操作方法和TAVP虚拟摄影棚控制系统（以下简称TAVP控制系统）的使用方法。以帮助用户迅速正确的使用该系统。此用户手册将分为两部分：TAVP客户端使用手册和TAVP控制系统使用手册。\n功能概述 TAVP虚拟摄影棚数字资产终端系统是时光坐标自主研发的一套能够快速预览、并下载使用数字资产系统，提供了更丰富、更真实、更易用的数字资产集，搭配IPAD控制工具可以实时对数字场景的光影变化进行调整，极大增强了对LED舞台操作的支持，在虚拟拍摄快节奏、高压力环境中，这套系统将极大提高拍摄效率。\nTAVP客户端使用手册 在给用户使用此终端系统之前，技术人员需要将系统正确部署在摄影棚的计算机集群中，并调试运行成功。 1、启动软件 双击“TAVPClient.exe”程序，进入软件的登录界面，如图1所示。输入正确的账号密码后，点击登录按钮。如果用户输入的账号密码错误或者是电脑没有宽带网络都会导致登录失败，且提示“账号或密码错误”。\n图 1\n2、主界面介绍 登录成功后就会进入主界面，首先显示的（1）部分是资产列表页，显示所有已经上传的资产，每个资产包含缩略图、名称、类别。，（2）部分是菜单，可以切换“资产列表”页和“我的资产”页。（3）部分表示搜索栏，支持模糊搜索。（4）部分表示资产类型筛选。（5）部分表示连接状态，绿色表示连接正常，红色表示连接失败。如图2所示。\n图 2 3、资产详情页 点击任意一个资产就会进入资产详情页，包含资产的预览缩略图、简介，出品方等信息，点击获取按钮即可将此资产添加到“我的资产”页中。如图3-2所示。 图 3 4、我的资产页 我的资产页中显示所有已经获取的资产，每个资产有若干个状态，比如下载、下载中、解压中、打开、关闭。如图4所示。此系统支持将资产添加到下载队列以及断电续传，下载过程中会显示进度，下载完毕后资产下方状态图标会变为打开状态。在一起准备就绪后，就可以点击打开任意一个场景了。根据场景的大小以及摄影棚配置的不同，打开场景的时间不同，点击打开按钮后，LED大屏会处于黑屏状态，此时要等待一段时间。\n图 4\nTAVP终端系统使用常见问题排查方法 1、登录失败：点击登录按钮提示“账号或者密码错误” ** 排查方法：** （1）、再三确认输入的账号密码是否正确（注意大小写）； （2）、检查PC主机是否接入了宽带网络； （3）、检查配置文件“TavpConfigInfo.ini”中的“tavp.ServerAddress”字段是否被修改过，默认服务器地址为：“http://123.60.70.27:80/”，配置文件通常在软件的根目录。（这一项通常由技术人员排查） 2、登录成功后右上角显示“连接失败” ** 排查方法：** （1）、检查各个渲染机器是否开始了TAVPListener服务器。 （2）、配置文件“TavpConfigInfo.ini”中的tavp.PrescribdRoute字段值是一个资产存储路径，检查此路径下是否有名为“TAVPNdisConfig”的文件。（这一项通常由技术人员排查） （3）检查“TAVPNdisConfig”的文件中的IP信息是否和摄影棚渲染机设备的IP是否一致。（这一项通常由技术人员排查） 3、点击打开按钮后LED屏幕长时间黑屏 ** 排查方法：** （1）、大场景一般都开的时间都比较长，请耐心等待。（例如四季森林在横店的打开时间为90秒） 3、点击打开按钮后ＬＥＤ屏幕没有反应 ** 排查方法：** （1）、请查看TAVPListener服务器上的log信息。（这一项通常由技术人员排查）\n４、下载过程中网络中断或者是意外断电：再次打开软件可继续之前的进度下载。 ５、LED远程控制系统无法控制调整TAVP终端系统打开的场景 ** 排查方法：** （1）、请检查运行LED远程控制程序的IPAD连接的无线网络是否和渲染主机处于同一个局域网内。通俗讲就是是否连接的是同一个ｗｉｆｉ。\n５、使用TAVP终端系统打开场景后，使用动捕的Tracker不能驱动内视锥 ** 排查方法：** （1）、请检查运行动捕（例如：青瞳）的机器有没有打开动捕的服务器和客户端 （2）、如果动捕的客户端已经打开，请检查动捕客户端软件中是否能够识别的处于摄影棚中的刚体Tracker。\nTAVP控制系统使用手册 功能概述 TAVP控制系统主要是为了实现远程控制虚拟场景中的一些环境以及LED中的其它参数。例如环境颜色和灯光氛围。 客户端 1、进入软件输入运行UE4实例的主机IP地址，点击连接进入控制页面，如图1-1所示。 图 5\n2、默认进入颜色校正界面，控制的是场景中的后处理相关参数，可依次控制整个场景中的全局部分、阴影部分、中间调部分、高光部分。其中每个页面包括饱和度、对比度、伽马、增益、偏移和曝光等，如图５所示。此外还可以控制整个场景的曝光，如图６所示，可控参数有曝光基数、最大亮度和最小亮度以及自适应的的速度值。 图 6 图 7 3、下一页面是灯光控制页面，主要控制UE4场景中的环境部分，其中包括天光、太阳光、大气雾等。天光部分可以控制的包括：HDR、强度、旋转和颜色；太阳光可以控制的包括：强度、色温、旋转；大气雾可以控制的包括：大气雾浓度、半衰减值、透明度和颜色；如图8、9、10所示。\n图 8 图 9 图 10\n4、如图11所示，舞台控制页面中主要是控制相机位置，其中包括相机的旋转和位移，除此之外还可以保存相机的位置，以及对位置信息的编辑。 图 11\n5、以下页面是屏幕相关控制，主要控制的有：LED内外视锥、LED辅助灯光系统、相机的焦点焦距。 控制LED的内外视锥主要包括内视锥大小和分辨率和外视锥分辨率，以及绿幕的显示和颜色调整。如图12所示。LED辅助灯光生成系统主要是用来手动生成光源的，利用 LED所谓辅助照明，其中可以控制灯光的强度、缩放、羽化和颜色。还可以对灯光进行选中和删除操作，如图13所示；还可以控制LED相机中的焦距和光圈大小，如图14所示。\n图 12 图 14 图 15\n6、此为模板页面，可以保存和应用场景中的环境数据，首次进入会自动获取一次场景中的环境数据，并作为默认数据。如果获取失败（网络没有连通）可稍后手动获取。模板分为两块，一个是后处理模板如图6-1所示，一个是灯光页面数据，如图16所示。对模板可进行的操作为添加模板、删除模板、选中和应用模板。最后一个页面可以自定义事件，来应对现场不同的需求，如图17。 使用方法： 1、开始进入场景后点击“Default”按钮，获取场景默认环境数据； 2、当场景进行了一些环境资产的调整后，点击“Add”按钮并输入模板名称可生成一个模板数据。 3、选中模板点击“Apply”按钮，可以应用模板数据，点击“Delete”按钮可以删除当前选中的模板数据，需要注意的是默认模板是不可选中也不可删除的。 图 16\n使用方法： 1、灯光模板对应着场景中的灯光关卡，点击不同的灯光模板即可切换不同的灯光关卡。（前提是在场景中设置有对应的灯光关卡）\n图 17\n（注：以上所有操作都需要先确保运行UE4实例的PC和移动端保持在统一局域网内！） 应用经验： 1、使用LED远程控制软件前要确保IPad和运行WebServer服务器的PC机处于同一个局域网内，可以使用电脑上的cmd命令行中的Ping命令，看是否能ping通IPad的上的IP地址。 2、点击连接后如果出现“成功获取数据”字样，说明成功连接服务器，否则需要重新检查是否开启了WebServer服务器。 3、在切换场景时，最好将客户端重新启动，避免使用上个场景中的数据。 4、如果连接成功，但是部分参数无法控制，需要检查服务器中的对应参数是否正确绑定。\n","permalink":"https://imrcao.top/posts/%E5%BD%92%E6%A1%A3/tavp%E8%99%9A%E6%8B%9F%E6%91%84%E5%BD%B1%E6%A3%9A%E6%95%B0%E5%AD%97%E8%B5%84%E4%BA%A7%E5%BA%93%E7%BB%88%E7%AB%AF%E7%B3%BB%E7%BB%9F%E7%94%A8%E6%88%B7%E6%89%8B%E5%86%8C./","summary":"前言 前言：《用户手册》将向用户介绍TAVP虚拟摄影棚数字资产库终端系统（以下简称TAVP客户端）操作方法和TAVP虚拟摄影棚控制系统（以下简称TAVP控制系统）的使用方法。以帮助用户迅速正确的使用该系统。此用户手册将分为两部分：TAVP客户端使用手册和TAVP控制系统使用手册。\n功能概述 TAVP虚拟摄影棚数字资产终端系统是时光坐标自主研发的一套能够快速预览、并下载使用数字资产系统，提供了更丰富、更真实、更易用的数字资产集，搭配IPAD控制工具可以实时对数字场景的光影变化进行调整，极大增强了对LED舞台操作的支持，在虚拟拍摄快节奏、高压力环境中，这套系统将极大提高拍摄效率。\nTAVP客户端使用手册 在给用户使用此终端系统之前，技术人员需要将系统正确部署在摄影棚的计算机集群中，并调试运行成功。 1、启动软件 双击“TAVPClient.exe”程序，进入软件的登录界面，如图1所示。输入正确的账号密码后，点击登录按钮。如果用户输入的账号密码错误或者是电脑没有宽带网络都会导致登录失败，且提示“账号或密码错误”。\n图 1\n2、主界面介绍 登录成功后就会进入主界面，首先显示的（1）部分是资产列表页，显示所有已经上传的资产，每个资产包含缩略图、名称、类别。，（2）部分是菜单，可以切换“资产列表”页和“我的资产”页。（3）部分表示搜索栏，支持模糊搜索。（4）部分表示资产类型筛选。（5）部分表示连接状态，绿色表示连接正常，红色表示连接失败。如图2所示。\n图 2 3、资产详情页 点击任意一个资产就会进入资产详情页，包含资产的预览缩略图、简介，出品方等信息，点击获取按钮即可将此资产添加到“我的资产”页中。如图3-2所示。 图 3 4、我的资产页 我的资产页中显示所有已经获取的资产，每个资产有若干个状态，比如下载、下载中、解压中、打开、关闭。如图4所示。此系统支持将资产添加到下载队列以及断电续传，下载过程中会显示进度，下载完毕后资产下方状态图标会变为打开状态。在一起准备就绪后，就可以点击打开任意一个场景了。根据场景的大小以及摄影棚配置的不同，打开场景的时间不同，点击打开按钮后，LED大屏会处于黑屏状态，此时要等待一段时间。\n图 4\nTAVP终端系统使用常见问题排查方法 1、登录失败：点击登录按钮提示“账号或者密码错误” ** 排查方法：** （1）、再三确认输入的账号密码是否正确（注意大小写）； （2）、检查PC主机是否接入了宽带网络； （3）、检查配置文件“TavpConfigInfo.ini”中的“tavp.ServerAddress”字段是否被修改过，默认服务器地址为：“http://123.60.70.27:80/”，配置文件通常在软件的根目录。（这一项通常由技术人员排查） 2、登录成功后右上角显示“连接失败” ** 排查方法：** （1）、检查各个渲染机器是否开始了TAVPListener服务器。 （2）、配置文件“TavpConfigInfo.ini”中的tavp.PrescribdRoute字段值是一个资产存储路径，检查此路径下是否有名为“TAVPNdisConfig”的文件。（这一项通常由技术人员排查） （3）检查“TAVPNdisConfig”的文件中的IP信息是否和摄影棚渲染机设备的IP是否一致。（这一项通常由技术人员排查） 3、点击打开按钮后LED屏幕长时间黑屏 ** 排查方法：** （1）、大场景一般都开的时间都比较长，请耐心等待。（例如四季森林在横店的打开时间为90秒） 3、点击打开按钮后ＬＥＤ屏幕没有反应 ** 排查方法：** （1）、请查看TAVPListener服务器上的log信息。（这一项通常由技术人员排查）\n４、下载过程中网络中断或者是意外断电：再次打开软件可继续之前的进度下载。 ５、LED远程控制系统无法控制调整TAVP终端系统打开的场景 ** 排查方法：** （1）、请检查运行LED远程控制程序的IPAD连接的无线网络是否和渲染主机处于同一个局域网内。通俗讲就是是否连接的是同一个ｗｉｆｉ。\n５、使用TAVP终端系统打开场景后，使用动捕的Tracker不能驱动内视锥 ** 排查方法：** （1）、请检查运行动捕（例如：青瞳）的机器有没有打开动捕的服务器和客户端 （2）、如果动捕的客户端已经打开，请检查动捕客户端软件中是否能够识别的处于摄影棚中的刚体Tracker。\nTAVP控制系统使用手册 功能概述 TAVP控制系统主要是为了实现远程控制虚拟场景中的一些环境以及LED中的其它参数。例如环境颜色和灯光氛围。 客户端 1、进入软件输入运行UE4实例的主机IP地址，点击连接进入控制页面，如图1-1所示。 图 5\n2、默认进入颜色校正界面，控制的是场景中的后处理相关参数，可依次控制整个场景中的全局部分、阴影部分、中间调部分、高光部分。其中每个页面包括饱和度、对比度、伽马、增益、偏移和曝光等，如图５所示。此外还可以控制整个场景的曝光，如图６所示，可控参数有曝光基数、最大亮度和最小亮度以及自适应的的速度值。 图 6 图 7 3、下一页面是灯光控制页面，主要控制UE4场景中的环境部分，其中包括天光、太阳光、大气雾等。天光部分可以控制的包括：HDR、强度、旋转和颜色；太阳光可以控制的包括：强度、色温、旋转；大气雾可以控制的包括：大气雾浓度、半衰减值、透明度和颜色；如图8、9、10所示。\n图 8 图 9 图 10","title":"TAVP虚拟摄影棚数字资产库终端系统用户手册."},{"content":"引言： 什么是引擎独立应用程序?让我们首先看一下虚幻引擎的Launcher运行器。不知道大家是否曾经好奇， 虚幻引擎的Launcher运行行器是用什么写的呢?如果你曾经仔细观察过虚幻引擎的运行器的文件结构，你会惊讶地发现，这个文件结构非常类似于虚幻引擎本身的文件结构，而非虚幻引擎编译打包形成的游戏的文件结构。这意味着，虚幻引擎的运行器是一个微型虚幻引擎，而不是一个打包形成的游戏! 如何开发这样的东西呢?如果我们希望在-个小型的、不是游戏的应用程序中继续使用虚幻引擎的一部分API，那么我们就需要学习虚幻引擎运行器这样的技术。\u0026mdash;此段引言自：《大象无形 虚幻引擎程序设计浅析》（如何开发一个简易版的独立程序后面再写，也可以参考这本书的第十四章）\n如何剥离独立程序：\n《大象无形》这本书中讲解了一种方法：一个最简单的方式就是去\\Engine\\Binaries\\Win 64文件夹下找到Blank Program .exe，拷贝到你的Epic运行器文件夹对应目录。没错， 就是虚幻引擎那个登录账号的运行器的目录。在笔者电脑上， 它位于Epic Games\\Launcher\\Engine\\Binaries\\Win 64。双击一下，你依然会发现程序能正常工作，验证了笔者的说法：你的程序不需要依赖一个引擎也能运行。\n但这种方法到目前为止貌似行不通了，我目前使用的版本是UE4-425源码版本，按照书本上的方法，点击应用程序发现会闪退。 另外一种比较笨的方法：直接拷贝文件。去\\Engine\\Binaries\\Win 64文件夹下找到你的EXE程序，将它按照源目录拷贝出去。例如：我这里创建的独立程序是一个服务器，起名为“DbServer”，则可以创建一个名为“DbServer”的文件夹（其实名称随意），但需要在这个文件夹下按照一定的文件夹结构进行创建，结构示意如图1-1所示：相关文件都要去引擎源码对应目录下去找。这样独立应用程序就可以运行了。 ","permalink":"https://imrcao.top/posts/%E5%BD%92%E6%A1%A3/%E5%A6%82%E4%BD%95%E5%89%A5%E7%A6%BBue4%E7%8B%AC%E7%AB%8B%E7%A8%8B%E5%BA%8F/","summary":"引言： 什么是引擎独立应用程序?让我们首先看一下虚幻引擎的Launcher运行器。不知道大家是否曾经好奇， 虚幻引擎的Launcher运行行器是用什么写的呢?如果你曾经仔细观察过虚幻引擎的运行器的文件结构，你会惊讶地发现，这个文件结构非常类似于虚幻引擎本身的文件结构，而非虚幻引擎编译打包形成的游戏的文件结构。这意味着，虚幻引擎的运行器是一个微型虚幻引擎，而不是一个打包形成的游戏! 如何开发这样的东西呢?如果我们希望在-个小型的、不是游戏的应用程序中继续使用虚幻引擎的一部分API，那么我们就需要学习虚幻引擎运行器这样的技术。\u0026mdash;此段引言自：《大象无形 虚幻引擎程序设计浅析》（如何开发一个简易版的独立程序后面再写，也可以参考这本书的第十四章）\n如何剥离独立程序：\n《大象无形》这本书中讲解了一种方法：一个最简单的方式就是去\\Engine\\Binaries\\Win 64文件夹下找到Blank Program .exe，拷贝到你的Epic运行器文件夹对应目录。没错， 就是虚幻引擎那个登录账号的运行器的目录。在笔者电脑上， 它位于Epic Games\\Launcher\\Engine\\Binaries\\Win 64。双击一下，你依然会发现程序能正常工作，验证了笔者的说法：你的程序不需要依赖一个引擎也能运行。\n但这种方法到目前为止貌似行不通了，我目前使用的版本是UE4-425源码版本，按照书本上的方法，点击应用程序发现会闪退。 另外一种比较笨的方法：直接拷贝文件。去\\Engine\\Binaries\\Win 64文件夹下找到你的EXE程序，将它按照源目录拷贝出去。例如：我这里创建的独立程序是一个服务器，起名为“DbServer”，则可以创建一个名为“DbServer”的文件夹（其实名称随意），但需要在这个文件夹下按照一定的文件夹结构进行创建，结构示意如图1-1所示：相关文件都要去引擎源码对应目录下去找。这样独立应用程序就可以运行了。 ","title":"如何剥离UE4独立程序"},{"content":"引言： 如何为你的游戏加入房间系统，可以进行加入、创建、搜索、销毁等功能？这段时间一直在查找相关的资料并不断进行各种测试，最终选择了配合UE4原生的DS服务器做房间系统。提到房间功能首先想到的是UE4的“Session”节点，使用“Create Session”等节点可以快速在局域网内搭建自己的房间系统，但是如果需要接入网络的话就比较麻烦，无论是选择“Online Subsystem Steam”还是选择“epic onLine Server”都是比较有难度的，当然后者应该是最优选择，它们内部都集成了房间、匹配、语音等比较完善的功能。我这边选择DS服务器采用“创建进程”方式来做房间系统，原因就是比较简单，对于新手来说是比较容易理解和入门的。下面主要记录一下开发房间系统的一些过程。\nDS服务器 DS服务器是UE4的专属服务器，服务器代码和游戏逻辑代码是写到一块的，关于如何打包DS服务器可以参考UE官方文档“（设置专属服务器）。 https://docs.unrealengine.com/4.27/zh-CN/InteractiveExperiences/Networking/HowTo/DedicatedServers/\n每启动一次DS服务器就相当于开启一个新的进程，也可以理解为创建了一个房间，那么我们可以通过启动多个DS服务器来做房间系统，客户端可以通过IP和端口号来加入DS服务器，也就是房间，端口号是自动分配的，我们需要手动指定，并不能和其它进程的端口重复即可。那么思路就明确了，在点击创建房间按钮的时候，需要启动一个DS服务器程序，并在代码中指定好端口，点击加入房间的时候，使用IP和端口号进入房间。退出房间就销毁进程。\n启动本地可执行程序 那么我们如何通过代码来启动外部程序呢？这里提供两种方式，第一种就是使用C标准库\u0026lt;stdlib.h\u0026gt;中的system()函数执行命令，和在CMD命令行中执行命令相似，执行结果如图1-1所示。参考链接如下。 https://www.runoob.com/cprogramming/c-function-system.html\n1 2 3 4 5 6 7 8 9 10 11 12 13 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;string.h\u0026gt; #include\u0026lt;stdlib.h\u0026gt; int main () { char command[50]; strcpy( command, \u0026#34;ls -l\u0026#34; ); system(command); return(0); } 还有一种方式是使用UE4原生模块“runtime”中的一个函数“FPlatformProcess::CreateProc()”，使用方法如下代码所示， #include\u0026quot;../Runtime/Core/Public/GenericPlatform/GenericPlatformProcess.h\u0026quot;\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void AMyProject2Character::TestOpenExe(FString URL, FString Params) { Handle_ = FPlatformProcess::CreateProc(*URL, *Params, true, false, false, nullptr, 0, nullptr, nullptr); //Handle_ = \u0026amp;currHandle; //FPlatformProcess:: UE_LOG(LogTemp, Warning, TEXT(\u0026#34;URL:::%s\u0026#34;), *URL); } void AMyProject2Character::TestCloseExe() { //FProcHandle currHandle = FPlatformProcess::CreateProc(*URL, nullptr, true, false, false, nullptr, 0, nullptr, nullptr); if (Handle_.IsValid()) { FPlatformProcess::TerminateProc(Handle_); Handle_.Reset(); } //FPlatformProcess::CloseProc(*Handle_); } 使用方法如下图所示，URL就是程序的路径，Param是参数，可以不填，我们这边需要指定地图，即创建房间前需要选择地图。指定启动方式，开始测试时可以log模式启动。最后需要指定端口号。 参考链接 https://blog.csdn.net/qq_31930499/article/details/88561678 https://zhuanlan.zhihu.com/p/162910207 https://www.lanindex.com/ue4-shootergame-standalone-dedicated-serverwindows-linux/ UE4 ShooterGame 项目源码分析 - Session的创建和加入： rhttps://blog.csdn.net/weixin_43405546/article/details/96163243?spm=1001.2101.3001.6650.6\u0026amp;utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7Edefault-6.nonecase\u0026amp;depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7Edefault-6.nonecaseGam rhttps://blog.csdn.net/weixin_43405546/article/details/96163243?spm=1001.2101.3001.6650.6\u0026amp;utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7Edefault-6.nonecase\u0026amp;depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7Edefault-6.nonecaseGam\n","permalink":"https://imrcao.top/posts/%E5%BD%92%E6%A1%A3/%E4%BD%BF%E7%94%A8ue4_ds%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%AE%9E%E7%8E%B0%E6%88%BF%E9%97%B4%E5%8A%9F%E8%83%BD/","summary":"引言： 如何为你的游戏加入房间系统，可以进行加入、创建、搜索、销毁等功能？这段时间一直在查找相关的资料并不断进行各种测试，最终选择了配合UE4原生的DS服务器做房间系统。提到房间功能首先想到的是UE4的“Session”节点，使用“Create Session”等节点可以快速在局域网内搭建自己的房间系统，但是如果需要接入网络的话就比较麻烦，无论是选择“Online Subsystem Steam”还是选择“epic onLine Server”都是比较有难度的，当然后者应该是最优选择，它们内部都集成了房间、匹配、语音等比较完善的功能。我这边选择DS服务器采用“创建进程”方式来做房间系统，原因就是比较简单，对于新手来说是比较容易理解和入门的。下面主要记录一下开发房间系统的一些过程。\nDS服务器 DS服务器是UE4的专属服务器，服务器代码和游戏逻辑代码是写到一块的，关于如何打包DS服务器可以参考UE官方文档“（设置专属服务器）。 https://docs.unrealengine.com/4.27/zh-CN/InteractiveExperiences/Networking/HowTo/DedicatedServers/\n每启动一次DS服务器就相当于开启一个新的进程，也可以理解为创建了一个房间，那么我们可以通过启动多个DS服务器来做房间系统，客户端可以通过IP和端口号来加入DS服务器，也就是房间，端口号是自动分配的，我们需要手动指定，并不能和其它进程的端口重复即可。那么思路就明确了，在点击创建房间按钮的时候，需要启动一个DS服务器程序，并在代码中指定好端口，点击加入房间的时候，使用IP和端口号进入房间。退出房间就销毁进程。\n启动本地可执行程序 那么我们如何通过代码来启动外部程序呢？这里提供两种方式，第一种就是使用C标准库\u0026lt;stdlib.h\u0026gt;中的system()函数执行命令，和在CMD命令行中执行命令相似，执行结果如图1-1所示。参考链接如下。 https://www.runoob.com/cprogramming/c-function-system.html\n1 2 3 4 5 6 7 8 9 10 11 12 13 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;string.h\u0026gt; #include\u0026lt;stdlib.h\u0026gt; int main () { char command[50]; strcpy( command, \u0026#34;ls -l\u0026#34; ); system(command); return(0); } 还有一种方式是使用UE4原生模块“runtime”中的一个函数“FPlatformProcess::CreateProc()”，使用方法如下代码所示， #include\u0026quot;../Runtime/Core/Public/GenericPlatform/GenericPlatformProcess.h\u0026quot;\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void AMyProject2Character::TestOpenExe(FString URL, FString Params) { Handle_ = FPlatformProcess::CreateProc(*URL, *Params, true, false, false, nullptr, 0, nullptr, nullptr); //Handle_ = \u0026amp;currHandle; //FPlatformProcess:: UE_LOG(LogTemp, Warning, TEXT(\u0026#34;URL:::%s\u0026#34;), *URL); } void AMyProject2Character::TestCloseExe() { //FProcHandle currHandle = FPlatformProcess::CreateProc(*URL, nullptr, true, false, false, nullptr, 0, nullptr, nullptr); if (Handle_.","title":"使用UE4_DS服务器实现房间功能"},{"content":"UCPT_HomePage继承自UUserWidget，在某个函数下使用LoadClass加载Content下的蓝图资产：\n1 2 3 4 void UCPT_HomePage::ConstructRecentProjectPanel(TArray\u0026lt;FString\u0026gt; TRecentProjectInfo) { TSubclassOf\u0026lt;UCPT_UnitProjectButton\u0026gt; UnitProjectButton = LoadClass\u0026lt;UCPT_UnitProjectButton\u0026gt;(nullptr, TEXT(\u0026#34;/Game/Blueprints/UI/UnitUI/WPT_UnitProjectButton.WPT_UnitProjectButton_C\u0026#34;)); } BUG：这样写在编辑器状态下没问题，可以正常加载资产，UnitProjectButton有效。但是打包后，UnitProjectButton是空的，也就是说无法加载指定路径的资产。（已验证：蓝图路径没问题，确实存在） 原因：搜索发现，导致此BUG的原因是，由于WPT_UnitProjectButton资产没有被外界引用，比如蓝图没有被显式引用（如直接拖入关卡或被资源表单引用），可能会被 Unreal 的打包流程优化掉。 解决方案：确保资源被打包了。 方案1：显式引用资源:在任意代码模块的构造函数中引用蓝图路径来确保加载。例如，添加一个加载步骤来触发依赖打包：\n1 2 3 4 5 6 7 8 9 UCPT_HomePage::UCPT_HomePage(const FObjectInitializer\u0026amp; ObjectInitializer) :Super(ObjectInitializer) { ConstructorHelpers::FClassFinder\u0026lt;UUserWidget\u0026gt; WPT_UnitProjectButton(TEXT(\u0026#34;/Game/Blueprints/UI/UnitUI/WPT_UnitProjectButton.WPT_UnitProjectButton_C\u0026#34;)); if (WPT_UnitProjectButton.Succeeded()) { UnitProjectButton = WPT_UnitProjectButton.Class; } } 方案2：使用 Cooking 设置加载资源 打开 Project Settings -\u0026gt; Packaging -\u0026gt; Advanced Settings。 在 Additional Asset Directories to Cook 添加你的 UnitUI 目录 (/Game/Blueprints/UI/UnitUI)。\n","permalink":"https://imrcao.top/posts/%E5%BD%92%E6%A1%A3/%E5%85%B3%E4%BA%8Eue4%E5%8A%A8%E6%80%81%E5%8A%A0%E8%BD%BDloadclass%E4%BD%BF%E7%94%A8%E6%97%A5%E5%BF%97/","summary":"UCPT_HomePage继承自UUserWidget，在某个函数下使用LoadClass加载Content下的蓝图资产：\n1 2 3 4 void UCPT_HomePage::ConstructRecentProjectPanel(TArray\u0026lt;FString\u0026gt; TRecentProjectInfo) { TSubclassOf\u0026lt;UCPT_UnitProjectButton\u0026gt; UnitProjectButton = LoadClass\u0026lt;UCPT_UnitProjectButton\u0026gt;(nullptr, TEXT(\u0026#34;/Game/Blueprints/UI/UnitUI/WPT_UnitProjectButton.WPT_UnitProjectButton_C\u0026#34;)); } BUG：这样写在编辑器状态下没问题，可以正常加载资产，UnitProjectButton有效。但是打包后，UnitProjectButton是空的，也就是说无法加载指定路径的资产。（已验证：蓝图路径没问题，确实存在） 原因：搜索发现，导致此BUG的原因是，由于WPT_UnitProjectButton资产没有被外界引用，比如蓝图没有被显式引用（如直接拖入关卡或被资源表单引用），可能会被 Unreal 的打包流程优化掉。 解决方案：确保资源被打包了。 方案1：显式引用资源:在任意代码模块的构造函数中引用蓝图路径来确保加载。例如，添加一个加载步骤来触发依赖打包：\n1 2 3 4 5 6 7 8 9 UCPT_HomePage::UCPT_HomePage(const FObjectInitializer\u0026amp; ObjectInitializer) :Super(ObjectInitializer) { ConstructorHelpers::FClassFinder\u0026lt;UUserWidget\u0026gt; WPT_UnitProjectButton(TEXT(\u0026#34;/Game/Blueprints/UI/UnitUI/WPT_UnitProjectButton.WPT_UnitProjectButton_C\u0026#34;)); if (WPT_UnitProjectButton.Succeeded()) { UnitProjectButton = WPT_UnitProjectButton.Class; } } 方案2：使用 Cooking 设置加载资源 打开 Project Settings -\u0026gt; Packaging -\u0026gt; Advanced Settings。 在 Additional Asset Directories to Cook 添加你的 UnitUI 目录 (/Game/Blueprints/UI/UnitUI)。","title":"关于UE4动态加载LoadClass使用日志"},{"content":"引言： 最近我的腾讯云服务器快到期了，正好看到腾讯云正在搞活动，新用户有很大优惠，就注册了一个新的账户，花了将近二百买了一个三年期限的轻量级服务器（2核4G），打了0.4折，还是非常值的。这样就需要把在老服务器上面部署的分布式和网站数据迁移到新的服务器上。简单找了一下好像不能跨账户进行镜像共享，所以只好从头开始配置服务器环境了，这里也正好记录一下。\n可以了解到什么： 配置VC++运行环境 安装设置Wamp集成环境 服务器的防火墙设置 配置Mysql环境，使本地可以Ping通并访问Mysql数据库 安装“Navicat Premium”并建立连接\n配置VC++运行环境 一个新的系统在初次打开EXE可执行程序时有可能会报错丢失各种“Dll”库，解决方法有很多，可以去网上下载对应的DLL并将其放到合适的位置，一般Win系统可以放在“C:\\Windows\\System32”下，这个需要根据自己系统的位数来定；也可以去下载继承环境的包，直接点击运行即可，我这里提供一个window服务器依赖环境的程序包（程序包见文章末尾的链接），点击运行进行相关操作即可，具体步骤如图１－１所示，先点击检测并修复，若此时运行EXE还是报错，就点击工具－选项－扩展－开始扩展，它会扩展基本的C++数据包。如果运行的是UE４打包出来的程序，当系统中缺少相关环境时，UE４的打包程序会自动启动一个安装VC++２０１５的程序。如果这些方法都不行，就装一个VS，勾选上C++相关选项即可。\n图１－１\n安装部署Wａｍｐ集成环境： 介绍 Wamp是Windows下的Apache+Mysql/MariaDB+Perl/PHP/Python，一组常用来搭建动态网站或者服务器的开源软件。软件网上即可下载，文章末尾的链接中也有软件安装包\n安装 官网下载安装包，安装Wamp，安装过程中如果有提示窗口直接点“否”即可。\n启动 管理员运行Wamp，软件会启动三个服务程序，全部启动成功后右下角图标会变成绿色，一般新的系统配置完成后，会直接变成绿色。\nwamp图标变为橙色 如果是红色或者是橙色则是只启动了一个或两个服务，如果你的系统中之前装有Mysql服务的话，可以去任务管理器中的“详细信息”中找到相关程序（例如：“mysqld.exe”）选中后将其结束任务，然后再重启Wamp即可。\n还有可能是因为80端口被占用导致的，比如这台服务器之前部署过本地的Web服务，都会导致80端口被占用，最直接的解决方案是找到Windows的IIS管理器，关闭所有服务，或者更改服务的端口，具体步骤如下： （1）、快捷键win+R 输入compmgmt.msc(打开计算机管理)，找到IIS，点击进入界面，如图所示：\n（2）、选择网站，单击右键，可以停止服务，也可以选择“编辑绑定”，修改成其它端口，如下图所示：\n设置wamp服务自启动 如果是云服务器可以不用设置自启动，因为服务器可以一直开着。如果是在本地部署的Wamp，想自启动的话可以按照以下步骤设置。 尝试过在C:\\ProgramData\\Microsoft\\Windows\\Start Menu\\Programs\\StartUp中放入wamp程序的快捷方式，测试发现程序并没有自启动，取消管理员运行也未能成功。尝试了多种方法，发现下面这个方案可行。 Win+R输入services.msc，会弹出服务面板，找到“wampapache64\u0026quot;,\u0026ldquo;wampmariadb64\u0026rdquo;,wampmysqld64\u0026quot;，单击右键，选择属性，在启动类型一栏中选择”自动“。设置好后重新启动计算机，你会发现右下角并没有wamp的图标，但是当你再次打开服务面板时你会发现这三个服务已经开启了，你可以测试访问一下本地的web服务。\n服务器的防火墙设置 1、在你的服务器上找到防火墙，我这里用的是腾讯云的服务器，点击添加规则，添加3306端口，一般服务器中会有MySQL的预设直接选择就行（如图１－２所示），这样在MySQL配置正确的情况下，其他客户端就可以链接MySQL服务器了。 ２、如果你需要其他的规则，可以添加自定义的。我这里就添加了UDP协议类型，端口是自定义的，但不能重复，我这里是为了测试一个游戏的服务器使用的临时端口。 图１－２\n配置Mysql 1、更改密码 初次部署好的WａｍｐServer，Mysql是没有密码的，首先进入命令台修改一下密码，默认用户名都是ｒｏｏｔ。点击MySQL找到MySQLconsole，进入命令行，如图１－３所示。 进入命令行后，它会提示输入密码，初始是没有密码的，直接点击空格即可（确保之前没有改过密码），更改密码的MySQL语句为： update mysql.user set authentication_string=password(\u0026rsquo;\u0026rsquo;) where user=\u0026rsquo;';\n需要注意的是不要使用下面这条语句，因为５.７版本的MySQL已经没有password这个字段了，改为了authentication_string update user set password=PASSWORD(\u0026lsquo;要修改的密码\u0026rsquo;) where user=\u0026lsquo;root\u0026rsquo;;\n完整的语句如下：\nmysql -u root -p Enter password: ******** Welcome to the MySQL monitor. Commands end with ; or \\g. Your MySQL connection id is 12 Server version: 5.7.18-log MySQL Community Server (GPL)\nCopyright (c) 2000, 2017, Oracle and/or its affiliates. All rights reserved.\nOracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners.\nType \u0026lsquo;help;\u0026rsquo; or \u0026lsquo;\\h\u0026rsquo; for help. Type \u0026lsquo;\\c\u0026rsquo; to clear the current input statement.\nmysql\u0026gt; use mysql; Database changed mysql\u0026gt; select User from user; #此处为查询用户命令 +\u0026mdash;\u0026mdash;\u0026mdash;\u0026ndash;+ | User | +\u0026mdash;\u0026mdash;\u0026mdash;\u0026ndash;+ | ******* | | mysql.sys | | root | +\u0026mdash;\u0026mdash;\u0026mdash;\u0026ndash;+ 3 rows in set (0.00 sec)\nmysql\u0026gt; update user set password=password(\u0026quot;\u0026quot;) where user=\u0026quot;\u0026quot;; #修改密码报错 ERROR 1054 (42S22): Unknown column \u0026lsquo;password\u0026rsquo; in \u0026lsquo;field list\u0026rsquo; mysql\u0026gt; update mysql.user set authentication_string=password(\u0026rsquo;\u0026rsquo;) where user=\u0026rsquo;\u0026rsquo;; #修改密码成功 Query OK, 1 row affected, 1 warning (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 1\nmysql\u0026gt; flush privileges; #立即生效 Query OK, 0 rows affected (0.00 sec)\nmysql\u0026gt; quit Bye\nn\u0026gt;mysql -u ******* -p #以该用户登录成功. Enter password: ******** MySQL 8.0后修改密码 MySQL 8.0后修改密码的官网连接 MySQL 8.0修改密码步骤: 以 root 用户登录MySQL。 进入MySQL系统自带数据库： mysql 数据库中。 执行更改密码语句。 退出MySQL后，使用新的密码重新登陆。 [root@localhost ~]# ./bin/mysql -u root -p \u0026lsquo;原来的密码\u0026rsquo;\nmysql\u0026gt; show databases;\nmysql\u0026gt; use mysql;\nmysql\u0026gt; ALTER USER \u0026lsquo;用户名\u0026rsquo;@\u0026rsquo;localhost\u0026rsquo; IDENTIFIED WITH mysql_native_password BY \u0026lsquo;新密码\u0026rsquo;;\nmysql\u0026gt; flush privileges; \u0026ndash;刷新MySQL的系统权限相关表\nmysql\u0026gt; exit;\n图１－３\n2、安装“Navicat Premium”并建立连接 Navicat Premium是一个可视化的MySQL操作软件，因为这个是收费软件，所以这里就不挂链接了，网上都可以下载到的。安装好之后新建一个Ｍｙｓｑｌ链接，输入用户名和刚才改的密码即可，如图１－４所示。 图１－４\n３、本地能ping通服务器，但不能访问服务器中的mysql数据库。在程序中访问MySQL数据库时有可能会失败，如图１－５所示。原因有下两点： 1、服务器没有开放3306端口；2、数据库用户权限不足 由于３３０６端口已经在服务器中开放了，那有可能时数据库用户的权限不足造成的。去Navicat上找到ｒｏｏｔ用户，由ｒｏｏｔ＠localｈｏｓｔ改为root＠％。‘%’意思是所有用户都可以接。‘localhost’只能本地连接。如图１－６所示。 图１－５ 图１－６\n","permalink":"https://imrcao.top/posts/%E5%BD%92%E6%A1%A3/%E4%BA%91%E6%9C%8D%E5%8A%A1%E5%99%A8%E4%B8%ADvc++%E5%92%8Cwamp%E7%9A%84%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE/","summary":"引言： 最近我的腾讯云服务器快到期了，正好看到腾讯云正在搞活动，新用户有很大优惠，就注册了一个新的账户，花了将近二百买了一个三年期限的轻量级服务器（2核4G），打了0.4折，还是非常值的。这样就需要把在老服务器上面部署的分布式和网站数据迁移到新的服务器上。简单找了一下好像不能跨账户进行镜像共享，所以只好从头开始配置服务器环境了，这里也正好记录一下。\n可以了解到什么： 配置VC++运行环境 安装设置Wamp集成环境 服务器的防火墙设置 配置Mysql环境，使本地可以Ping通并访问Mysql数据库 安装“Navicat Premium”并建立连接\n配置VC++运行环境 一个新的系统在初次打开EXE可执行程序时有可能会报错丢失各种“Dll”库，解决方法有很多，可以去网上下载对应的DLL并将其放到合适的位置，一般Win系统可以放在“C:\\Windows\\System32”下，这个需要根据自己系统的位数来定；也可以去下载继承环境的包，直接点击运行即可，我这里提供一个window服务器依赖环境的程序包（程序包见文章末尾的链接），点击运行进行相关操作即可，具体步骤如图１－１所示，先点击检测并修复，若此时运行EXE还是报错，就点击工具－选项－扩展－开始扩展，它会扩展基本的C++数据包。如果运行的是UE４打包出来的程序，当系统中缺少相关环境时，UE４的打包程序会自动启动一个安装VC++２０１５的程序。如果这些方法都不行，就装一个VS，勾选上C++相关选项即可。\n图１－１\n安装部署Wａｍｐ集成环境： 介绍 Wamp是Windows下的Apache+Mysql/MariaDB+Perl/PHP/Python，一组常用来搭建动态网站或者服务器的开源软件。软件网上即可下载，文章末尾的链接中也有软件安装包\n安装 官网下载安装包，安装Wamp，安装过程中如果有提示窗口直接点“否”即可。\n启动 管理员运行Wamp，软件会启动三个服务程序，全部启动成功后右下角图标会变成绿色，一般新的系统配置完成后，会直接变成绿色。\nwamp图标变为橙色 如果是红色或者是橙色则是只启动了一个或两个服务，如果你的系统中之前装有Mysql服务的话，可以去任务管理器中的“详细信息”中找到相关程序（例如：“mysqld.exe”）选中后将其结束任务，然后再重启Wamp即可。\n还有可能是因为80端口被占用导致的，比如这台服务器之前部署过本地的Web服务，都会导致80端口被占用，最直接的解决方案是找到Windows的IIS管理器，关闭所有服务，或者更改服务的端口，具体步骤如下： （1）、快捷键win+R 输入compmgmt.msc(打开计算机管理)，找到IIS，点击进入界面，如图所示：\n（2）、选择网站，单击右键，可以停止服务，也可以选择“编辑绑定”，修改成其它端口，如下图所示：\n设置wamp服务自启动 如果是云服务器可以不用设置自启动，因为服务器可以一直开着。如果是在本地部署的Wamp，想自启动的话可以按照以下步骤设置。 尝试过在C:\\ProgramData\\Microsoft\\Windows\\Start Menu\\Programs\\StartUp中放入wamp程序的快捷方式，测试发现程序并没有自启动，取消管理员运行也未能成功。尝试了多种方法，发现下面这个方案可行。 Win+R输入services.msc，会弹出服务面板，找到“wampapache64\u0026quot;,\u0026ldquo;wampmariadb64\u0026rdquo;,wampmysqld64\u0026quot;，单击右键，选择属性，在启动类型一栏中选择”自动“。设置好后重新启动计算机，你会发现右下角并没有wamp的图标，但是当你再次打开服务面板时你会发现这三个服务已经开启了，你可以测试访问一下本地的web服务。\n服务器的防火墙设置 1、在你的服务器上找到防火墙，我这里用的是腾讯云的服务器，点击添加规则，添加3306端口，一般服务器中会有MySQL的预设直接选择就行（如图１－２所示），这样在MySQL配置正确的情况下，其他客户端就可以链接MySQL服务器了。 ２、如果你需要其他的规则，可以添加自定义的。我这里就添加了UDP协议类型，端口是自定义的，但不能重复，我这里是为了测试一个游戏的服务器使用的临时端口。 图１－２\n配置Mysql 1、更改密码 初次部署好的WａｍｐServer，Mysql是没有密码的，首先进入命令台修改一下密码，默认用户名都是ｒｏｏｔ。点击MySQL找到MySQLconsole，进入命令行，如图１－３所示。 进入命令行后，它会提示输入密码，初始是没有密码的，直接点击空格即可（确保之前没有改过密码），更改密码的MySQL语句为： update mysql.user set authentication_string=password(\u0026rsquo;\u0026rsquo;) where user=\u0026rsquo;';\n需要注意的是不要使用下面这条语句，因为５.７版本的MySQL已经没有password这个字段了，改为了authentication_string update user set password=PASSWORD(\u0026lsquo;要修改的密码\u0026rsquo;) where user=\u0026lsquo;root\u0026rsquo;;\n完整的语句如下：\nmysql -u root -p Enter password: ******** Welcome to the MySQL monitor. Commands end with ; or \\g. Your MySQL connection id is 12 Server version: 5.","title":"云服务器中VC++和Wamp的环境配置"},{"content":"前言 这一节会介绍WebSocket协议，和在UE4中如何使用WebSocket进行通讯。引擎中内置了现成的WebSockets模块和第三方库“libWebSockets”，接下来会调用现有模块来创建两个插件，分别负责创建WebSocket服务器和WebSocket客户端。 为什么是WebSocket\n介绍 WebSocket是一种全双工、双向通信协议，设计用于在单个TCP连接上进行实时通信。 WebSocket特点 1、双向通信：客户端和服务器可以在任何时间相互发送消息，而不需要轮询或长轮询。 2、低延迟：因为它建立在TCP连接之上，并且避免了HTTP请求和响应的开销，WebSocket通信具有较低的延迟。 3、持久连接：WebSocket连接一旦建立，可以持续使用，直到客户端或服务器主动关闭连接。 工作原理 1、握手阶段：WebSocket通信开始时，客户端发起一个标准的HTTP请求，包含一些特殊的头信息，表明它希望升级到WebSocket协议。 2、协议升级：如果服务器支持WebSocket协议，它会通过HTTP响应确认升级请求。完成握手后，HTTP连接升级为WebSocket连接。 3、数据传输：握手完成后，客户端和服务器之间可以通过WebSocket协议进行全双工通信，可以传输文本和二进制等格式消息。\n使用场景 WebSocket协议适用于需要实时更新和低延迟的应用，如： 1、即时通讯：如聊天应用和消息系统。 2、实时通知：如股票价格更新、体育比分等。 3、在线游戏：需要快速、频繁的状态更新。 4、协同编辑：如多人同时编辑文档、表格等。 和HTTP区别 1、HTTP有一个缺陷就是通讯只能由客户端发起，它们只能单向请求，如果客户端需要监测服务器中会连续变化的某一参数，我们只能使用“轮询”，即没隔一段时间就发送一个查询请求，这种轮询效率比较低，且浪费资源。 2、WebSocket连接一旦建立，客户端和服务器都可以主动发送消息给彼此，即它是一种双向通讯。\n总结 WebSocket协议提供了一种高效、低延迟的双向通信方式，适用于需要实时数据传输的应用场景。通过WebSocket，客户端和服务器可以在单个持久TCP连接上自由地交换数据，而不必受到传统HTTP请求/响应模式的限制。 客户端实现 Unreal Engine中的“Runtime/Online/下有一个”WebSocket“模块，模块中的IWebSocket接口提供了客户端常用的功能，连接、关闭、发送消息等。 1、创建插件后在.Build.cs中加入”WebSockets“模块\n1 2 3 4 5 6 7 8 PublicDependencyModuleNames.AddRange( new string[] { \u0026#34;Core\u0026#34;, \u0026#34;WebSockets\u0026#34;,. } ); 2、我这里创建一个继承自UObject的类：UWSClients。WSClients.h实现如下： /********************************************************* *\n@copyright @author Imrcao @date 2024年06月27日16:25:00 @brief 创建一个WebSocket客户端，支持Win64，Win32，IOS，MacOS，Linux平台 @See **********************************************************/\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 #pragma once #include \u0026#34;CoreMinimal.h\u0026#34; #include \u0026#34;UObject/NoExportTypes.h\u0026#34; #include \u0026#34;WSClients.generated.h\u0026#34; /** * WebSocketClient */ UCLASS(BlueprintType) class WEBSOCKETCLIENT_API UWSClients : public UObject { GENERATED_BODY() public: UWSClients(); ~UWSClients(); public: virtual void BeginDestroy() override; public: UFUNCTION(BlueprintCallable) UWSClients* GetorCreateWsClientInstance(); UFUNCTION(BlueprintCallable, Category = \u0026#34;WSClient\u0026#34;) bool InitAndConnect(FString WsAddr); UFUNCTION(BlueprintCallable, Category = \u0026#34;WSClient\u0026#34;) void Send(const FString\u0026amp; Data); void Send(const void* Data, SIZE_T Size, bool bIsBinary = false); UFUNCTION() void OnConnected(); UFUNCTION() void OnConnectedError(const FString\u0026amp; Error); UFUNCTION() void OnClosed(int32 StatusCode, const FString\u0026amp; Reason, bool bWasClean); UFUNCTION() void OnMessage(const FString\u0026amp; MessageString); UFUNCTION() void OnMessageSent(const FString\u0026amp; MessageString); //UFUNCTION() void OnRawMessage(const void* data, SIZE_T Size, SIZE_T BytesRemaining); private: UWSClients* WSClientInstance; TSharedPtr\u0026lt;class IWebSocket\u0026gt; WebSocket; }; 3、WSClients.cpp实现如下。起初我是创建了一个全局静态的指针变量，将这个函数GetorCreateWsClientInstance写成了静态函数，可以直接在蓝图中调用了，这样做的话会有个问题，在UE编辑器模式下，由于引擎对静态成员函数声明周期的管理，退出游戏时，并不会执行析构或者BeginDestroy函数，这就意味着连接无法自动关闭。打包后执行一切都是正常的。为了方便测试这里就不用静态函数，后面在蓝图中直接用ConstructObjectFromClass构建一个UObject。 // Fill out your copyright notice in the Description page of Project Settings.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 #include \u0026#34;WSClients.h\u0026#34; #include \u0026#34;WebSocketsModule.h\u0026#34; #include \u0026#34;IWebSocket.h\u0026#34; //UWSClients* UWSClients::WSClientInstance = nullptr; UWSClients::UWSClients() { WebSocket = nullptr; } UWSClients::~UWSClients() { } void UWSClients::BeginDestroy() { Super::BeginDestroy(); if (WebSocket \u0026amp;\u0026amp; WebSocket-\u0026gt;IsConnected()) { WebSocket-\u0026gt;Close(); } } UWSClients* UWSClients::GetorCreateWsClientInstance() { WSClientInstance = WSClientInstance == nullptr ? NewObject\u0026lt;UWSClients\u0026gt;() : WSClientInstance; WSClientInstance-\u0026gt;AddToRoot(); if (!WSClientInstance) { GEngine-\u0026gt;AddOnScreenDebugMessage(-1, 3.f, FColor::Red, \u0026#34;ClientInstance is NULL!\u0026#34;); } return WSClientInstance; } bool UWSClients::InitAndConnect(FString WsAddr) { if (!FModuleManager::Get().IsModuleLoaded(\u0026#34;WebSockets\u0026#34;)) { FModuleManager::Get().LoadModule(\u0026#34;WebSockets\u0026#34;); } WebSocket = FWebSocketsModule::Get().CreateWebSocket(WsAddr); WebSocket-\u0026gt;OnConnected().AddUObject(this, \u0026amp;UWSClients::OnConnected); WebSocket-\u0026gt;OnConnectionError().AddUObject(this, \u0026amp;UWSClients::OnConnectedError); WebSocket-\u0026gt;OnClosed().AddUObject(this, \u0026amp;UWSClients::OnClosed); WebSocket-\u0026gt;OnMessage().AddUObject(this, \u0026amp;UWSClients::OnMessage); WebSocket-\u0026gt;OnMessageSent().AddUObject(this, \u0026amp;UWSClients::OnMessageSent); WebSocket-\u0026gt;OnRawMessage().AddUObject(this, \u0026amp;UWSClients::OnRawMessage); WebSocket-\u0026gt;Connect(); return !!WebSocket; } void UWSClients::Send(const FString\u0026amp; Data) { WebSocket-\u0026gt;Send(Data); } void UWSClients::Send(const void* Data, SIZE_T Size, bool bIsBinary) { } void UWSClients::OnConnected() { GEngine-\u0026gt;AddOnScreenDebugMessage(-1, 15.0f, FColor::Green, \u0026#34;Successfully connected\u0026#34;); } void UWSClients::OnConnectedError(const FString\u0026amp; Error) { GEngine-\u0026gt;AddOnScreenDebugMessage(-1, 15.0f, FColor::Red, Error); } void UWSClients::OnClosed(int32 StatusCode, const FString\u0026amp; Reason, bool bWasClean) { GEngine-\u0026gt;AddOnScreenDebugMessage(-1, 15.0f, bWasClean ? FColor::Green : FColor::Red, \u0026#34;Connection closed \u0026#34; + Reason); } void UWSClients::OnMessage(const FString\u0026amp; MessageString) { GEngine-\u0026gt;AddOnScreenDebugMessage(-1, 15.0f, FColor::Cyan, \u0026#34;Received message: \u0026#34; + MessageString); } void UWSClients::OnMessageSent(const FString\u0026amp; MessageString) { GEngine-\u0026gt;AddOnScreenDebugMessage(-1, 15.0f, FColor::Yellow, \u0026#34;Sent message: \u0026#34; + MessageString); } void UWSClients::OnRawMessage(const void* data, SIZE_T Size, SIZE_T BytesRemaining) { } 3、蓝图中使用如下图所示 服务端实现 网上大多数的教程都是教怎么写客户端的，关于如何在UE中搭建一个支持WebSocket的服务器，资料很少。其实引擎中有一个现成的插件：WebSocketNetworking 1、同样在.Build.cs文件中加入模块：WebSocketNetworking，并在.uplugin文件中启用插件模块： \u0026ldquo;Plugins\u0026rdquo;: [ { \u0026ldquo;Name\u0026rdquo;: \u0026ldquo;WebSocketNetworking\u0026rdquo;, \u0026ldquo;Enabled\u0026rdquo;: true } ] 2、创建一个继承自UObject的类：UWSServer。服务器需要时刻监听外部客户端的连接，所有需要一个Tick函数。继承自UObject的类，在实例后不会加入引擎中的Tick，AActor和UActorComponent都会加入Tick，如果你的对象也需要每帧去Tick（一般来说是什么Mgr管理器之类的全局单例对象），也非常简单：再继承多一个抽象类FTickableGameObject，重写实现几个纯虚函数即可：\n1 2 3 4 5 6 7 8 9 10 11 public: virtual void Tick(float DeltaTime) override; virtual bool IsTickable() const override; virtual TStatId GetStatId() const override; //cpp void UTickObject::Tick(float DeltaTime) {} bool UTickObject::IsTickable() const {return true;} TStatId UTickObject::GetStatId() const {return Super::GetStatID();} 3、WSServer.h文件定义如下：我这声明了一个动态多播代理FOnReceiveClientMessage，在蓝图中绑定，C++中执行，用于当服务器收到消息后，将此消息转发给其它客户端，实现多个客户端之间信息同步的功能。需要注意的是，动态单播代理和静态代理是不支持UE的反射系统的，即你无法使用宏UPROPERTY(BlueprintAssignable/BlueprintCallable)标记. /********************************************************* *\n@copyright @author Imrcao @date 2024年06月27日16:25:00 @brief 创建一个WebSocket服务器，支持Win64，Win32，Linux平台 @See **********************************************************/\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 #pragma once #include \u0026#34;CoreMinimal.h\u0026#34; #include \u0026#34;UObject/NoExportTypes.h\u0026#34; #include \u0026#34;Tickable.h\u0026#34; #include \u0026#34;UObject/StrongObjectPtr.h\u0026#34; #include \u0026#34;Modules/ModuleManager.h\u0026#34; #include \u0026#34;INetworkingWebSocket.h\u0026#34; #include \u0026#34;Engine.h\u0026#34; #include \u0026#34;WSServer.generated.h\u0026#34; /** * */ DECLARE_MULTICAST_DELEGATE_OneParam(FOnConnectionClosed, FGuid /*ClientId*/); DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnReceiveClientMessage, FString, Message, FGuid, SenderGuid); UCLASS(BlueprintType) class WEBSOCKETCS_API UWSServer : public UObject, public FTickableGameObject { GENERATED_BODY() public: UWSServer(); ~UWSServer(); public: //virtual void PostInitProperties() override; virtual void BeginDestroy() override; virtual void Tick(float DeltaTime) override; virtual bool IsTickable() const override; virtual TStatId GetStatId() const override; public: UFUNCTION(BlueprintCallable, Category = \u0026#34;WSServer\u0026#34;) static UWSServer* GetOrCreateWSServer(); UFUNCTION(BlueprintCallable, Category = \u0026#34;WSServer\u0026#34;) bool StartServer(int32 Port); UFUNCTION(BlueprintCallable, Category = \u0026#34;WSServer\u0026#34;) void StopServer(); //给所有客户端发送消息 UFUNCTION(BlueprintCallable, Category = \u0026#34;WSServer\u0026#34;) void SendMessagesToAllClients(const FString Msg); //给指定客户端发送消息 UFUNCTION(BlueprintCallable, Category = \u0026#34;WSServer\u0026#34;) void SendMessagesSpecifyClient(const FGuid\u0026amp; ClientGUID, const TArray\u0026lt;uint8\u0026gt;\u0026amp; InUTF8Payload); //转发消息给其它所有客户端（不包含发送端） UFUNCTION(BlueprintCallable, Category = \u0026#34;WSServer\u0026#34;) void ForwardMessagesToOtherClients(const FGuid\u0026amp; SenderGUID, const FString Msg); bool IsRunning() const; bool WSServerTick(float DeltaTime); //FOnConnectionClosed\u0026amp; OnConnectionClosed() { return OnConnectionClosedDelegate; } public: UPROPERTY(BlueprintAssignable, Category = \u0026#34;MyCategory\u0026#34;) FOnReceiveClientMessage OnReceiveClientMessage; FOnConnectionClosed OnConnectionClosedDelegate; private: void OnWebSocketClientConnected(INetworkingWebSocket* Socket); void ReceivedRawPacket(void* Data, int32 Size, FGuid ClientId); void OnSocketClose(INetworkingWebSocket* Socket); private: class FWebSocketConnection { public: explicit FWebSocketConnection(INetworkingWebSocket* InSocket) : Socket(InSocket) , Id(FGuid::NewGuid()) { } FWebSocketConnection(FWebSocketConnection\u0026amp;\u0026amp; WebSocketConnection) : Id(WebSocketConnection.Id) { Socket = WebSocketConnection.Socket; WebSocketConnection.Socket = nullptr; } ~FWebSocketConnection() { if (Socket) { delete Socket; Socket = nullptr; } } FWebSocketConnection(const FWebSocketConnection\u0026amp;) = delete; FWebSocketConnection\u0026amp; operator=(const FWebSocketConnection\u0026amp;) = delete; FWebSocketConnection\u0026amp; operator=(FWebSocketConnection\u0026amp;\u0026amp;) = delete; /** Underlying WebSocket. */ INetworkingWebSocket* Socket = nullptr; /** Generated ID for this client. */ FGuid Id; }; private: static UWSServer* ServerInstance; TUniquePtr\u0026lt;class IWebSocketServer\u0026gt; Server; TArray\u0026lt;FWebSocketConnection\u0026gt; Connections; }; 3、WSServer.cpp实现如下： // Fill out your copyright notice in the Description page of Project Settings.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 #include \u0026#34;WSServer.h\u0026#34; #include \u0026#34;Containers/Ticker.h\u0026#34; #include \u0026#34;IPAddress.h\u0026#34; #include \u0026#34;IWebSocketNetworkingModule.h\u0026#34; #include \u0026#34;WebSocketNetworkingDelegates.h\u0026#34; #include \u0026#34;WebSocketNetDriver.h\u0026#34; #include \u0026#34;IWebSocketServer.h\u0026#34; #include \u0026lt;string\u0026gt; UWSServer* UWSServer::ServerInstance = nullptr; UWSServer::UWSServer() { } UWSServer::~UWSServer() { } void UWSServer::BeginDestroy() { Super::BeginDestroy(); StopServer(); ServerInstance = nullptr; } void UWSServer::Tick(float DeltaTime) { WSServerTick(DeltaTime); } bool UWSServer::IsTickable() const { return true; } TStatId UWSServer::GetStatId() const { return Super::GetStatID(); } UWSServer* UWSServer::GetOrCreateWSServer() { ServerInstance = ServerInstance == nullptr ? NewObject\u0026lt;UWSServer\u0026gt;() : ServerInstance; ServerInstance-\u0026gt;AddToRoot(); if (!ServerInstance) { GEngine-\u0026gt;AddOnScreenDebugMessage(-1, 3.f, FColor::Red, \u0026#34;ServerInstance is NULL!\u0026#34;); } return ServerInstance; } bool UWSServer::StartServer(int32 Port) { FWebSocketClientConnectedCallBack CallBack; CallBack.BindUObject(this, \u0026amp;UWSServer::OnWebSocketClientConnected); Server = FModuleManager::Get().LoadModuleChecked\u0026lt;IWebSocketNetworkingModule\u0026gt;(TEXT(\u0026#34;WebSocketNetworking\u0026#34;)).CreateServer(); if (!Server || !Server-\u0026gt;Init(Port, CallBack)) { Server.Reset(); GEngine-\u0026gt;AddOnScreenDebugMessage(-1, 3.f, FColor::Red, \u0026#34;Server Startup Failed!\u0026#34;); return false; } GEngine-\u0026gt;AddOnScreenDebugMessage(-1, 3.f, FColor::Green, FString::Printf(TEXT(\u0026#34;Server Startup Success by the port: %d\u0026#34;), Port)); return true; } void UWSServer::StopServer() { if (IsRunning()) { Server.Reset(); } } bool UWSServer::IsRunning() const { /* *在C++中，两个感叹号（!!）前缀用于将一个表达式的值显式地转换为布尔值。 这是一种惯用法，用于确保表达式的结果是一个明确的布尔值（true 或 false）。 *Server 是一个指针，如果它是非空（即指向有效对象），那么 Server 的值是非零的。 !Server 将这个非零值转换为 false，如果 Server 是空指针（即值为零），则 !Server 将返回 true。 再次应用一个感叹号（即 !!Server），将 false 转换为 true，将 true 转换为 false。 最终结果就是将 Server 的值转换为布尔值，如果 Server 是非空指针，结果是 true，如果 Server 是空指针，结果是 false。 */\n1 2 3 4 5 6 return !!Server; } void UWSServer::OnWebSocketClientConnected(INetworkingWebSocket* Socket) { /*ensureMsgf 是 Unreal Engine 4 中用于断言（assertion）的一种宏。断言用于在开发过程中检测程序中的错误条件。 如果条件为 false，则会触发断言，通常会记录一条错误信息并在调试器中中断程序执行，以便开发人员可以立即注意到并修复问题。 ensureMsgf 类似于 ensure，但是它允许你提供一个格式化的错误消息，以便在条件不满足时显示更多的调试信息。 / if (ensureMsgf(Socket, TEXT(\u0026ldquo;Socket was null while creating a new websocket connection.\u0026rdquo;))) { /{ int32 RawRemoteAddr; Socket-\u0026gt;GetRawRemoteAddr(RawRemoteAddr); GEngine-\u0026gt;AddOnScreenDebugMessage(-1, 3.f, FColor::Green, FString::Printf(TEXT(\u0026ldquo;RawRemoteAddr: %d\u0026rdquo;), RawRemoteAddr));\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 FString LocalEndPoint = Socket-\u0026gt;LocalEndPoint(true); GEngine-\u0026gt;AddOnScreenDebugMessage(-1, 3.f, FColor::Green, FString::Printf(TEXT(\u0026#34;LocalEndPoint: %s\u0026#34;), *LocalEndPoint)); FString RemoteEndPoint = Socket-\u0026gt;RemoteEndPoint(true); GEngine-\u0026gt;AddOnScreenDebugMessage(-1, 3.f, FColor::Green, FString::Printf(TEXT(\u0026#34;RemoteEndPoint: %s\u0026#34;), *RemoteEndPoint)); }*/ FWebSocketConnection Connection = FWebSocketConnection{ Socket }; FWebSocketPacketReceivedCallBack ReceiveCallBack; ReceiveCallBack.BindUObject(this, \u0026amp;UWSServer::ReceivedRawPacket, Connection.Id); Socket-\u0026gt;SetReceiveCallBack(ReceiveCallBack); FWebSocketInfoCallBack CloseCallback; CloseCallback.BindUObject(this, \u0026amp;UWSServer::OnSocketClose, Socket); Socket-\u0026gt;SetSocketClosedCallBack(CloseCallback); //Socket-\u0026gt; Connections.Add(MoveTemp(Connection)); } } void UWSServer::ReceivedRawPacket(void* Data, int32 Size, FGuid ClientId) { TArrayView\u0026lt;uint8\u0026gt; dataArrayView = MakeArrayView(static_cast\u0026lt;uint8*\u0026gt;(Data), Size); const std::string cstr(reinterpret_cast\u0026lt;const char*\u0026gt;(dataArrayView.GetData()), dataArrayView.Num()); FString frameAsFString = UTF8_TO_TCHAR(cstr.c_str()); OnReceiveClientMessage.Broadcast(frameAsFString, ClientId); } void UWSServer::SendMessagesToAllClients(const FString Msg) { FTCHARToUTF8 utf8Str(*Msg); int32 utf8StrLen = utf8Str.Length(); TArray\u0026lt;uint8\u0026gt; uint8Array; uint8Array.SetNum(utf8StrLen); memcpy(uint8Array.GetData(), utf8Str.Get(), utf8StrLen); for (auto\u0026amp; ws : Connections) { ws.Socket-\u0026gt;Send(uint8Array.GetData(), uint8Array.Num(), /*PrependSize=*/false); } } void UWSServer::SendMessagesSpecifyClient(const FGuid\u0026amp; ClientGUID, const TArray\u0026lt;uint8\u0026gt;\u0026amp; InUTF8Payload) { /* * FindByPredicate是TArray的一个成员函数，用于查找满足指定条件的第一个元素。 * FindByPredicate函数接受一个谓词函数作为参数。这个谓词函数用于定义查找条件。 * 在下面的函数中谓词函数是一个捕获ClientGUID的lambda表达式： * [\u0026amp;ClientGUID](const FWebSocketConnection\u0026amp; InConnection) { return InConnection.Id == ClientGUID; } */ if (FWebSocketConnection* Connection = Connections.FindByPredicate([\u0026amp;ClientGUID](const FWebSocketConnection\u0026amp; InConnection) { return InConnection.Id == ClientGUID; })) { Connection-\u0026gt;Socket-\u0026gt;Send(InUTF8Payload.GetData(), InUTF8Payload.Num(), /*PrependSize=*/false); } } void UWSServer::ForwardMessagesToOtherClients(const FGuid\u0026amp; SenderGUID, const FString Msg) { FTCHARToUTF8 utf8Str(*Msg); int32 utf8StrLen = utf8Str.Length(); TArray\u0026lt;uint8\u0026gt; uint8Array; uint8Array.SetNum(utf8StrLen); memcpy(uint8Array.GetData(), utf8Str.Get(), utf8StrLen); for (auto\u0026amp; ws : Connections) { if (ws.Id != SenderGUID) { ws.Socket-\u0026gt;Send(uint8Array.GetData(), uint8Array.Num(), /*PrependSize=*/false); } } } bool UWSServer::WSServerTick(float DeltaTime) { if (IsRunning()) { Server-\u0026gt;Tick(); return true; } else { return false; } } void UWSServer::OnSocketClose(INetworkingWebSocket* Socket) { int32 Index = Connections.IndexOfByPredicate([Socket](const FWebSocketConnection\u0026amp; Connection) { return Connection.Socket == Socket; }); GEngine-\u0026gt;AddOnScreenDebugMessage(-1, 3.f, FColor::Red, FString::Printf(TEXT(\u0026#34;OnSocketClose: %d\u0026#34;), Index)); if (Index != INDEX_NONE) { OnConnectionClosedDelegate.Broadcast(Connections[Index].Id); Connections.RemoveAtSwap(Index); } } 4、蓝图使用如图所示： 参考链接 https://blog.csdn.net/ljason1993/article/details/123031678 https://docs.unrealengine.com/4.27/zh-CN/ProductionPipelines/ScriptingAndAutomation/WebControl/RemoteControlAPIWebsocketReference/ https://www.bilibili.com/video/BV18a4y1c74N/?spm_id_from=333.337.search-card.all.click\u0026amp;vd_source=4f9d167ab1a8f301688a50d993ad691a\n","permalink":"https://imrcao.top/posts/%E5%BD%92%E6%A1%A3/%E5%9C%A8ue4%E4%B8%AD%E4%BD%BF%E7%94%A8websocket/","summary":"前言 这一节会介绍WebSocket协议，和在UE4中如何使用WebSocket进行通讯。引擎中内置了现成的WebSockets模块和第三方库“libWebSockets”，接下来会调用现有模块来创建两个插件，分别负责创建WebSocket服务器和WebSocket客户端。 为什么是WebSocket\n介绍 WebSocket是一种全双工、双向通信协议，设计用于在单个TCP连接上进行实时通信。 WebSocket特点 1、双向通信：客户端和服务器可以在任何时间相互发送消息，而不需要轮询或长轮询。 2、低延迟：因为它建立在TCP连接之上，并且避免了HTTP请求和响应的开销，WebSocket通信具有较低的延迟。 3、持久连接：WebSocket连接一旦建立，可以持续使用，直到客户端或服务器主动关闭连接。 工作原理 1、握手阶段：WebSocket通信开始时，客户端发起一个标准的HTTP请求，包含一些特殊的头信息，表明它希望升级到WebSocket协议。 2、协议升级：如果服务器支持WebSocket协议，它会通过HTTP响应确认升级请求。完成握手后，HTTP连接升级为WebSocket连接。 3、数据传输：握手完成后，客户端和服务器之间可以通过WebSocket协议进行全双工通信，可以传输文本和二进制等格式消息。\n使用场景 WebSocket协议适用于需要实时更新和低延迟的应用，如： 1、即时通讯：如聊天应用和消息系统。 2、实时通知：如股票价格更新、体育比分等。 3、在线游戏：需要快速、频繁的状态更新。 4、协同编辑：如多人同时编辑文档、表格等。 和HTTP区别 1、HTTP有一个缺陷就是通讯只能由客户端发起，它们只能单向请求，如果客户端需要监测服务器中会连续变化的某一参数，我们只能使用“轮询”，即没隔一段时间就发送一个查询请求，这种轮询效率比较低，且浪费资源。 2、WebSocket连接一旦建立，客户端和服务器都可以主动发送消息给彼此，即它是一种双向通讯。\n总结 WebSocket协议提供了一种高效、低延迟的双向通信方式，适用于需要实时数据传输的应用场景。通过WebSocket，客户端和服务器可以在单个持久TCP连接上自由地交换数据，而不必受到传统HTTP请求/响应模式的限制。 客户端实现 Unreal Engine中的“Runtime/Online/下有一个”WebSocket“模块，模块中的IWebSocket接口提供了客户端常用的功能，连接、关闭、发送消息等。 1、创建插件后在.Build.cs中加入”WebSockets“模块\n1 2 3 4 5 6 7 8 PublicDependencyModuleNames.AddRange( new string[] { \u0026#34;Core\u0026#34;, \u0026#34;WebSockets\u0026#34;,. } ); 2、我这里创建一个继承自UObject的类：UWSClients。WSClients.h实现如下： /********************************************************* *\n@copyright @author Imrcao @date 2024年06月27日16:25:00 @brief 创建一个WebSocket客户端，支持Win64，Win32，IOS，MacOS，Linux平台 @See **********************************************************/\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 #pragma once #include \u0026#34;CoreMinimal.","title":"在UE4中使用WebSocket"},{"content":"想要在网络中传输媒体资源，就需要将媒体资源转为二进制数据。这里以简单的图片为例，将UE4中的UTexture2D资产转为TArray类型。这篇笔记不详细讲原理，只放代码。\n**读取图片的颜色信息\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 bool UTextureToolsBPLibrary::TextureToColorData(TArray\u0026lt;FColor\u0026gt;\u0026amp; colorDataPtr, int\u0026amp; outW, int\u0026amp; outh, UTexture2D* Texture) { colorDataPtr.Reset(); outW = outh = 0; if (!Texture)return false; FIntPoint Size = FIntPoint::ZeroValue; bool bReadSuccess = true; TArray\u0026lt;uint8\u0026gt; RawData; EPixelFormat Format; bool bInEditorExec = false; TArray\u0026lt;uint8*\u0026gt; RawData2; Size = FIntPoint(Texture-\u0026gt;GetPlatformData()-\u0026gt;SizeX, Texture-\u0026gt;GetPlatformData()-\u0026gt;SizeY); RawData2.AddZeroed(Texture-\u0026gt;GetNumMips()); Texture-\u0026gt;GetMipData(0, (void**)RawData2.GetData()); const EPixelFormat NewFormat = Texture-\u0026gt;GetPixelFormat(); if (Texture-\u0026gt;GetPlatformData()-\u0026gt;Mips.Num() == 0) { bReadSuccess = false; } if (NewFormat == PF_B8G8R8A8) { Format = PF_B8G8R8A8; } else if (NewFormat == PF_FloatRGBA) { Format = PF_FloatRGBA; } else { bReadSuccess = false; } //Put first mip data into usable array if (bReadSuccess) { //这里的二进制数据RawData，无法直接使用KismetRenderingLibrary::ImportBufferAsTexture()将其转为Texture，会提示未指定图片的格式。 uint32 TotalSize = Texture-\u0026gt;GetPlatformData()-\u0026gt;Mips[0].BulkData.GetElementCount(); RawData.AddZeroed(TotalSize); FMemory::Memcpy(RawData.GetData(), RawData2[0], TotalSize); } //Deallocate the mip data for (auto MipData : RawData2) { FMemory::Free(MipData); } outW = Size.X; outh = Size.Y; uint32 datalen = outW * outh; if (datalen \u0026lt;= 1)return false; colorDataPtr.Init(FColor::White, datalen); if (!bReadSuccess)return false; uint32 temp = 0; for (uint32 i = 0; i \u0026lt; datalen; i++) { //逐素拷贝 colorDataPtr[i].B = RawData[temp]; temp++; colorDataPtr[i].G = RawData[temp]; temp++; colorDataPtr[i].R = RawData[temp]; temp++; colorDataPtr[i].A = RawData[temp]; temp++; } return true; } 将颜色信息转为二进制数据\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 bool UTextureToolsBPLibrary::ColorDataToBinaryData(TArray\u0026lt;uint8\u0026gt;\u0026amp; OutDatas, TArray\u0026lt;FColor\u0026gt; SourceColorData, int SizeX, int SizeY, int32 Quality) { if (SizeX * SizeY \u0026lt;= 0 || SourceColorData.Num() \u0026lt; 1)return false; IImageWrapperModule\u0026amp; ImageWrapperModule = FModuleManager::LoadModuleChecked\u0026lt;IImageWrapperModule\u0026gt;(FName(\u0026#34;ImageWrapper\u0026#34;)); TSharedPtr\u0026lt;IImageWrapper\u0026gt; ImageWrapper = ImageWrapperModule.CreateImageWrapper(EImageFormat::JPEG); int l = SizeX * SizeY * sizeof(FColor); if (ImageWrapper-\u0026gt;SetRaw(SourceColorData.GetData(), l, SizeX, SizeY, ERGBFormat::BGRA, 8)) { OutDatas.Empty(); OutDatas = ImageWrapper-\u0026gt;GetCompressed(Quality); ImageWrapper.Reset(); return true; } ImageWrapper.Reset(); return false; } 包含模块和头文件\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include \u0026#34;IImageWrapper.h\u0026#34; #include \u0026#34;IImageWrapperModule.h\u0026#34; //.Build.cs文件中包含 \u0026#34;ImageWrapper\u0026#34;模块 PrivateDependencyModuleNames.AddRange( new string[] { \u0026#34;CoreUObject\u0026#34;, \u0026#34;Engine\u0026#34;, \u0026#34;Slate\u0026#34;, \u0026#34;SlateCore\u0026#34;, \u0026#34;ImageWrapper\u0026#34;, // ... add private dependencies that you statically link with here ... } ); 将二进制数据转为UTexture2D数据 //#include \u0026ldquo;Kismet/KismetRenderingLibrary.h\u0026rdquo; //可以使用UKismetRenderingLibrary库中现有的函数 UTexture2D* UKismetRenderingLibrary::ImportBufferAsTexture2D(UObject* WorldContextObject, const TArray\u0026amp; Buffer) 读取RenderTarget2D的颜色信息(TArray) //#include \u0026ldquo;Kismet/KismetRenderingLibrary.h\u0026rdquo; //可以使用UKismetRenderingLibrary库中现有的函数\n1 bool UKismetRenderingLibrary::ReadRenderTarget(UObject* WorldContextObject, UTextureRenderTarget2D* TextureRenderTarget, TArray\u0026lt;FColor\u0026gt;\u0026amp; OutSamples, bool bNormalize) 总结 有了这些函数可以将图片在网络中进行传输，\n","permalink":"https://imrcao.top/posts/%E5%BD%92%E6%A1%A3/utexture2d%E8%BD%AC%E4%BA%8C%E8%BF%9B%E5%88%B6%E6%95%B0%E6%8D%AE/","summary":"想要在网络中传输媒体资源，就需要将媒体资源转为二进制数据。这里以简单的图片为例，将UE4中的UTexture2D资产转为TArray类型。这篇笔记不详细讲原理，只放代码。\n**读取图片的颜色信息\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 bool UTextureToolsBPLibrary::TextureToColorData(TArray\u0026lt;FColor\u0026gt;\u0026amp; colorDataPtr, int\u0026amp; outW, int\u0026amp; outh, UTexture2D* Texture) { colorDataPtr.","title":"UTexture2D转二进制数据"},{"content":"引言：最近要换服务器，需要将部署在旧服务器上的数据迁移到新的服务器上。咨询了腾讯云的客服，由于我是跨账户迁移且两个账户的地域不一致，所以不能使用共享镜像的方式，那就只能手动迁移数据了。每个人使用的环境和工具不同，方法自然也不同，下面主要整理一下步骤以及记录一下在这个过程中遇到了一些问题。\n步骤： 1、服务器wordpress(网站)本地数据的拷贝； 2、MySQL数据库数据迁移（巨坑）； 3、MySQL数据表wp_options中部分字段数据更改； 4、进入WordPress后台； 5、网站备案迁移； 6、新服务器上的备案迁入和备案变更； 7、域名重新解析； 8、等待备案完成（1-3个工作日）；\n1、服务器wordpress(网站)本地数据的拷贝； 我这里使用的wampserver集成环境，环境配置建议查看文章(服务器VC++和Mysql环境置)。首先去旧服务器上在wamp的安装目录中找到“www”文件夹，压缩并拷贝到新服务器的wamp对应的目录下。如图1-1所示；\n图1-1\n2、MySQL数据库数据迁移（巨坑）； 数据库的迁移按道理说是非常简单的，直接导出、运行sql文件就行了，但不知道是什么原因运行sql文件的时候总是报错导致数据和结构都进不来，我这里用的MySQL版本为5.7.24，使用版本“5.5.5-10.3.12-MariaDB”就可以直接运行同一个sql文件，很诡异，不想改变版本的情况下只能再想其它办法迁移数据了。 尝试了很多方法，最后长到了一种勉强可以用的方法吧，为什么说是勉强可以用呢，因为它还是不能完美的把数据和结构迁移过来。使用“导出”的方式，如图2-1所示，有很过格式供你选择，多尝试几个格式发现其中两个格式还是挺好用的分别为“.mdb”和\u0026quot;.dbf\u0026quot;。 在导入的时候要注意：“源表”和“目标表”要一致，如图2-2所示。如果在新服务器中的数据库中没有这个数据表，则在“新建表”下会打对勾✔，表示会新建一个MySQL数据表。 最后你可能还会发现数据导入丢失和数据表结构不对，此时我是手动调整的，如果你有好的方法欢迎私信交流。对于数据丢失的情况可以手动复制粘贴，如图2-3所示。数据表结构不对的话可以手动进行复制和粘贴。 总之，无论用什么方法，最终目的就是将旧服务器中MySQL的数据迁移到新的服务器中。\n图2-1\n图2-2\n图2-3\n3、MySQL数据表wp_options中部分字段数据更改； 数据迁移完毕后找到数据表“wp_options”，找到字段“option_name”和“option_value”，更改“siterul”和“home”对应的值为“http://127.0.0.1”，如图3-1所示：\n图3-1\n4、进入WordPress后台； 更改完毕后就可以在浏览器中输入“http://127.0.0.1”进入主页，输入“http://127.0.0.1/wp-login.php”进入登录页面，输入之前WordPress的账户和密码就可以进入wordpress后台。如图4-1所示。 图4-1\n5、网站备案迁移； 以上步骤就完成了数据的迁移，目前网站只能在服务器上的本地运行查看，还不能通过域名进入。因为账号换了所以备案也需要迁移，进入旧服务器中的控制台，找到备案-\u0026gt;更多-\u0026gt;迁移备案账号，如图5-1所示。输入相关信息即可。\n图5-1\n6、新服务器上的备案迁入和备案变更； 备案迁移完成后，在新服务器中就可以看到备案信息了，此时备案中的信息还是之前服务器中的公网IP，要改成现在服务器的公网IP，需要“变更备案”，如图6-1所示。这个过程输入的资料和信息还是很多的。\n图6-1\n7、域名重新解析； 变更备案完成后，最后需要对域名进行重新解析，如图7-1所示\n图7-1\n8、等待备案完成（1-3个工作日）； 等待备案完成后即可通过域名访问自己的网站了。\n","permalink":"https://imrcao.top/posts/%E5%BD%92%E6%A1%A3/%E8%B7%A8%E8%B4%A6%E6%88%B7%E8%BF%81%E7%A7%BB%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99%E6%95%B0%E6%8D%AE/","summary":"引言：最近要换服务器，需要将部署在旧服务器上的数据迁移到新的服务器上。咨询了腾讯云的客服，由于我是跨账户迁移且两个账户的地域不一致，所以不能使用共享镜像的方式，那就只能手动迁移数据了。每个人使用的环境和工具不同，方法自然也不同，下面主要整理一下步骤以及记录一下在这个过程中遇到了一些问题。\n步骤： 1、服务器wordpress(网站)本地数据的拷贝； 2、MySQL数据库数据迁移（巨坑）； 3、MySQL数据表wp_options中部分字段数据更改； 4、进入WordPress后台； 5、网站备案迁移； 6、新服务器上的备案迁入和备案变更； 7、域名重新解析； 8、等待备案完成（1-3个工作日）；\n1、服务器wordpress(网站)本地数据的拷贝； 我这里使用的wampserver集成环境，环境配置建议查看文章(服务器VC++和Mysql环境置)。首先去旧服务器上在wamp的安装目录中找到“www”文件夹，压缩并拷贝到新服务器的wamp对应的目录下。如图1-1所示；\n图1-1\n2、MySQL数据库数据迁移（巨坑）； 数据库的迁移按道理说是非常简单的，直接导出、运行sql文件就行了，但不知道是什么原因运行sql文件的时候总是报错导致数据和结构都进不来，我这里用的MySQL版本为5.7.24，使用版本“5.5.5-10.3.12-MariaDB”就可以直接运行同一个sql文件，很诡异，不想改变版本的情况下只能再想其它办法迁移数据了。 尝试了很多方法，最后长到了一种勉强可以用的方法吧，为什么说是勉强可以用呢，因为它还是不能完美的把数据和结构迁移过来。使用“导出”的方式，如图2-1所示，有很过格式供你选择，多尝试几个格式发现其中两个格式还是挺好用的分别为“.mdb”和\u0026quot;.dbf\u0026quot;。 在导入的时候要注意：“源表”和“目标表”要一致，如图2-2所示。如果在新服务器中的数据库中没有这个数据表，则在“新建表”下会打对勾✔，表示会新建一个MySQL数据表。 最后你可能还会发现数据导入丢失和数据表结构不对，此时我是手动调整的，如果你有好的方法欢迎私信交流。对于数据丢失的情况可以手动复制粘贴，如图2-3所示。数据表结构不对的话可以手动进行复制和粘贴。 总之，无论用什么方法，最终目的就是将旧服务器中MySQL的数据迁移到新的服务器中。\n图2-1\n图2-2\n图2-3\n3、MySQL数据表wp_options中部分字段数据更改； 数据迁移完毕后找到数据表“wp_options”，找到字段“option_name”和“option_value”，更改“siterul”和“home”对应的值为“http://127.0.0.1”，如图3-1所示：\n图3-1\n4、进入WordPress后台； 更改完毕后就可以在浏览器中输入“http://127.0.0.1”进入主页，输入“http://127.0.0.1/wp-login.php”进入登录页面，输入之前WordPress的账户和密码就可以进入wordpress后台。如图4-1所示。 图4-1\n5、网站备案迁移； 以上步骤就完成了数据的迁移，目前网站只能在服务器上的本地运行查看，还不能通过域名进入。因为账号换了所以备案也需要迁移，进入旧服务器中的控制台，找到备案-\u0026gt;更多-\u0026gt;迁移备案账号，如图5-1所示。输入相关信息即可。\n图5-1\n6、新服务器上的备案迁入和备案变更； 备案迁移完成后，在新服务器中就可以看到备案信息了，此时备案中的信息还是之前服务器中的公网IP，要改成现在服务器的公网IP，需要“变更备案”，如图6-1所示。这个过程输入的资料和信息还是很多的。\n图6-1\n7、域名重新解析； 变更备案完成后，最后需要对域名进行重新解析，如图7-1所示\n图7-1\n8、等待备案完成（1-3个工作日）； 等待备案完成后即可通过域名访问自己的网站了。","title":"跨账户迁移个人网站数据"},{"content":"前言： （写在前面的一些废话）最近在写给手机发送短信的一个功能，之前一直在用“创蓝253短信平台”，调用起来也是非常简单，根据域名构建一个Http请求即可。但是最近这个平台对签名审核变得非常的严格且对一些平台和APP是不发的（应该是因为这些小平台的安全性没有做好，才会这么严格），申请的时候还需要 APP的下载地址、官网、设计图纸等等，付款还需要公对公，这对于我们这些独立开发者来说很不友好，我提供了公司的营业执照（一个朋友的空壳公司）和部分APP的设计图纸（做的一个交友类小游戏），申请了三四次都给我驳回了，联系客服又给我说交友类、棋牌类游戏不发（一万个草泥马）。决定还是研究腾讯的短信服务吧，但是腾讯的短信API调用起来不是一般的麻烦（由于腾讯的SDK不支持在Windows环境，所以只能硬着头皮调用API了）。下面主要记录一下在调用腾讯短信API中的一些主要步骤。\n最终目的： 使用UE4调用腾讯的发送短信API，给手机发送短信。\n介绍腾讯云短信服务及用“API Explorer\u0026quot;在线发送验证码 官方文档上有详细的文档说明，这里就不多赘述，这里主要是总结一下用法。我用的V3.0版本的API，使用 API的核心就是通过一些加密算法构建“签名串”，这个签名串用于构建HTTP请求，加密算法主要用的是“Opessll库”里面的部分函数，这个库的用法我单独写了一篇文章，参考“在Windows下配置Openssl环境”。 在测试阶段推荐使用“API Explorer”，这个可以在线发送验证码，生成签名串。发送验证码需要开通短信服务并申请签名和签名模板，还要申请腾讯SDK的秘钥和 ID（每个用户最多申请两个秘钥和 ID，）这部分官方文档有详细步骤，参考我当时发送的界面如图1-1所示。“API Explorer”生成的签名串可以用来验证你的生成签名串的程序，如图1-2所示。\n使用\u0026quot;Postman\u0026quot;测试发送Http/Https请求 在使用C++编写HTTP请求之前，可以先使用软件“Postman”发送测试请求，以确保请求串没有错误。使用方法如图1-3所示： 使用纯C++项目生成“签名串” 创建一个C++的空项目，添加一个CPP文件后，将官网的代码粘贴进去（官方文档链接：https://cloud.tencent.com/document/product/382/52072#C.2B.2B），参考文章“在Windows下配置Openssl环境”配好环境后，还需要改一个名叫“get_data”函数，如下面示例所示，之后应该就可以编译通过了。官网示例使用的接口不是发送短信接口，所以需要稍微改动一下代码。发送短信的部分改动代码如下：\n函数改动： string get_data(int64_t\u0026amp; timestamp) {\nint64_t ii = 1231232133; string utcDate; char buff[20] = { 0 }; //改动了这里，牵扯到time_t类型的初始化 time_t timenow = (time_t)(timestamp);\n1 2 3 4 5 6 7 8 9 10 11 struct tm sttime; //sttime = *gmtime(\u0026amp;timestamp); gmtime_s(\u0026amp;sttime, \u0026amp;timenow); //gmtime_s(, ); strftime(buff, sizeof(buff), \u0026#34;%Y-%m-%d\u0026#34;, \u0026amp;sttime); utcDate = string(buff); return utcDate; } SendSms接口，生成字符串所必要的一些参数： // 密钥参数 string SECRET_ID = \u0026ldquo;AKI**evvfCU\u0026rdquo;; string SECRET_KEY = \u0026ldquo;3pLaZrkNkFTQ6VdU5PS\u0026rdquo;;\nstring service = \u0026ldquo;sms\u0026rdquo;; string host = \u0026ldquo;sms.tencentcloudapi.com\u0026rdquo;; string region = \u0026ldquo;ap-nanjing\u0026rdquo;; string action = \u0026ldquo;SendSms\u0026rdquo;; string version = \u0026ldquo;2021-01-11\u0026rdquo;; int64_t timestamp = 1635181318; string date = get_data(timestamp);\n// ************* 步骤 1：拼接规范请求串 ************* string httpRequestMethod = \u0026ldquo;POST\u0026rdquo;; string canonicalUri = \u0026ldquo;/\u0026rdquo;; string canonicalQueryString = \u0026ldquo;\u0026rdquo;; //string canonicalHeaders = \u0026ldquo;content-type:application/json; charset=utf-8\\nhost:\u0026rdquo; + host + \u0026ldquo;\\n\u0026rdquo;; string canonicalHeaders = \u0026ldquo;content-type:application/json\\nhost:\u0026rdquo; + host + \u0026ldquo;\\n\u0026rdquo;; string signedHeaders = \u0026ldquo;content-type;host\u0026rdquo;; //string payload = \u0026ldquo;{\u0026quot;Limit\u0026quot;: 1, \u0026quot;Filters\u0026quot;: [{\u0026quot;Values\u0026quot;: [\u0026quot;\\u672a\\u547d\\u540d\u0026quot;], \u0026quot;Name\u0026quot;: \u0026quot;instance-name\u0026quot;}]}\u0026rdquo;; string payload = \u0026ldquo;{\u0026quot;PhoneNumberSet\u0026quot;:[\u0026quot;+86134\u0026quot;],\u0026quot;SmsSdkAppId\u0026quot;:\u0026quot;1445\u0026quot;,\u0026quot;SignName\u0026quot;:\u0026quot;Io\u0026quot;,\u0026quot;TemplateId\u0026quot;:\u0026quot;93*79\u0026quot;,\u0026quot;TemplateParamSet\u0026quot;:[\u0026quot;564131\u0026quot;]}\u0026rdquo;; string hashedRequestPayload = sha256Hex(payload); string canonicalRequest = httpRequestMethod + \u0026ldquo;\\n\u0026rdquo; + canonicalUri + \u0026ldquo;\\n\u0026rdquo; + canonicalQueryString + \u0026ldquo;\\n\u0026rdquo;\ncanonicalHeaders + \u0026ldquo;\\n\u0026rdquo; + signedHeaders + \u0026ldquo;\\n\u0026rdquo; + hashedRequestPayload; cout \u0026laquo; canonicalRequest \u0026laquo; endl; 1 2 完整的.cpp文件如下： 使用UE4C++项目生成“签名串” 1、使用UE4原生模块发送简单Http请求(熟悉HTTP的可以跳过) 我这里使用UE4-426版本，创建一个空的C++项目，然后创建一个Library插件。要发送Http请求首先要添加相关模块，在“.Build.cs”文件中添加“HTTP”模块。\n1 2 3 4 5 6 7 8 9 10 11 12 PrivateDependencyModuleNames.AddRange( new string[] { \u0026#34;CoreUObject\u0026#34;, \u0026#34;Engine\u0026#34;, \u0026#34;Slate\u0026#34;, \u0026#34;SlateCore\u0026#34;, \u0026#34;HTTP\u0026#34;, // ... add private dependencies that you statically link with here ... } ) （写给对HTTP不熟悉的新手）在写发送HTTP程序之前我们思考一下，如何才能确定HTTP请求成功了呢？一般的，请求应该放在客户端中进行，对应的应该还有一个服务端用来接收并处理数据。这里可以参考UE4官方文档中的“网页远程控制-\u0026gt;远程控制快速入门”（链接如下），网页远程控制系统在虚幻引擎中运行了一个网页服务器（详情参考链接），此文档会教你如何创建一个工程工程并启动服务器，还会使用“Postman”软件测试服务器。服务器没问题后，就可以用我们写的HTTP请求程序向服务器发送相关请求，用来验证写的HTTP请求程序。\nhttps://docs.unrealengine.com/4.27/zh-CN/ProductionPipelines/ScriptingAndAutomation/WebControl/QuickStart/\n测试代码如下（向UE4引擎运行的网页服务器发送请求），如果请求成功，UE4引擎中应该会有相应的变化，在阅读这一段之前建议先把“远程控制快速入门”跟着做一遍\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 #include \u0026#34;SimpleHttp.h\u0026#34; void USimpleHttpBPLibrary::SendSimpleHttpRespont() { TSharedPtr\u0026lt;IHttpRequest\u0026gt; HttpReuest = FHttpModule::Get().CreateRequest(); FString URL = \u0026#34;http://127.0.0.1:8080/remote/object/call\u0026#34;; FString Data = \u0026#34;{\\\u0026#34;objectPath\\\u0026#34;:\\\u0026#34;/Game/ThirdPersonBP/Maps/ThirdPersonExampleMap.ThirdPersonExampleMap:PersistentLevel.SkySphereBlueprint\\\u0026#34;,\\\u0026#34;functionName\\\u0026#34;:\\\u0026#34;RefreshMaterial\\\u0026#34;,\\\u0026#34;generateTransaction\\\u0026#34;:true}\u0026#34;; HttpReuest-\u0026gt;SetVerb(\u0026#34;PUT\u0026#34;); HttpReuest-\u0026gt;SetHeader(\u0026#34;Content-Type\u0026#34;, \u0026#34;application/json\u0026#34;); //HttpReuest-\u0026gt;SetHeader(\u0026#34;User-Agent\u0026#34;, \u0026#34;application/x-www-form-urlencoded;charset=utf-8\u0026#34;); HttpReuest-\u0026gt;SetURL(URL); HttpReuest-\u0026gt;SetContentAsString(Data); HttpReuest-\u0026gt;OnProcessRequestComplete().BindStatic(\u0026amp;USimpleHttpBPLibrary::OnRequestComplete); if (HttpReuest-\u0026gt;ProcessRequest()) { GEngine-\u0026gt;AddOnScreenDebugMessage(-1,5.0f,FColor::Blue, \u0026#34;Success\u0026#34;); } else { GEngine-\u0026gt;AddOnScreenDebugMessage(-1, 5.0f, FColor::Blue, \u0026#34;Faile\u0026#34;); } ｝ void USimpleHttpBPLibrary::OnRequestComplete(FHttpRequestPtr HttpRePtr, FHttpResponsePtr HttpResPtr, bool Success) { auto fd = HttpRePtr-\u0026gt;GetContent(); FString tt = HttpResPtr-\u0026gt;GetContentAsString(); GEngine-\u0026gt;AddOnScreenDebugMessage(-1, 5.0f, FColor::Blue, tt); } 2、在 UE4项目中配置Openssl环境（UE4中引用第三方链接库）。 在UE4项目中无法按照文章“在Windows下配置Openssl环境”配置相关环境，UE4的项目属性中没有“链接器-\u0026gt;输入”，所以无法指定第三方静态链接库。UE4引用第三方静态链接库需要在“.Build.cs”文件中配置。 首先在插件的根目录的下创建文件夹“ThirdParty”，然后去“Openssl”的安装目录下找到文件夹“OpenSSL-Win64”（我这里的目录为“C:\\Program Files\\OpenSSL-Win64”），最后将这个文件夹复制到“ThirdParty”中，并保留文件夹“Include”、“Lib”两个文件夹，其余全删除。如图1-4所示。\n打开插件中的“.Build.cs”文件，进行一些IO操作，将第三方静态链接库自动应用。完整代码如下： // Some copyright should be here\u0026hellip; using System.IO; using UnrealBuildTool;\npublic class SimpleHttp : ModuleRules { /\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;Begin\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;/ private string ModulePath { // get { return Path.GetDirectoryName(RulesCompiler.GetModuleFilename(this.GetType().Name)); } get { return ModuleDirectory; } }\nprivate string ThirdPartyPath { get { return Path.GetFullPath(Path.Combine(ModulePath, \u0026ldquo;../../ThirdParty/\u0026rdquo;)); } } private string MyLibPath //第三方库MyTestLib的目录 { get { return Path.GetFullPath(Path.Combine(ThirdPartyPath, \u0026ldquo;OpenSSL-Win64\u0026rdquo;)); } } /\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;End\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;/\npublic SimpleHttp(ReadOnlyTargetRules Target) : base(Target) { PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 PublicIncludePaths.AddRange( new string[] { // ... add public include paths required here ... } ); PrivateIncludePaths.AddRange( new string[] { // ... add other private include paths required here ... } ); PublicDependencyModuleNames.AddRange( new string[] { \u0026#34;Core\u0026#34;, // ... add other public dependencies that you statically link with here ... } ); PrivateDependencyModuleNames.AddRange( new string[] { \u0026#34;CoreUObject\u0026#34;, \u0026#34;Engine\u0026#34;, \u0026#34;Slate\u0026#34;, \u0026#34;SlateCore\u0026#34;, \u0026#34;HTTP\u0026#34;, // ... add private dependencies that you statically link with here ... } ); DynamicallyLoadedModuleNames.AddRange( new string[] { // ... add any modules that your module loads dynamically here ... } ); /*------------------Begin------------------*/ LoadThirdPartyLib(Target); /*------------------End------------------*/ } /*------------------Begin------------------*/ public bool LoadThirdPartyLib(ReadOnlyTargetRules Target) { bool isLibrarySupported = false; if ((Target.Platform == UnrealTargetPlatform.Win64) || (Target.Platform == UnrealTargetPlatform.Win32))//平台判断 { isLibrarySupported = true; System.Console.WriteLine(\u0026#34;----- isLibrarySupported true\u0026#34;); //string PlatformSubPath = (Target.Platform == UnrealTargetPlatform.Win64) ? \u0026#34;Win64\u0026#34; : \u0026#34;Win32\u0026#34;; string LibrariesPath = Path.Combine(MyLibPath, \u0026#34;Lib\u0026#34;); PublicAdditionalLibraries.Add(Path.Combine(LibrariesPath,/* PlatformSubPath,*/ \u0026#34;libssl.lib\u0026#34;));//加载第三方静态库.lib PublicAdditionalLibraries.Add(Path.Combine(LibrariesPath,/* PlatformSubPath,*/ \u0026#34;libcrypto.lib\u0026#34;));//加载第三方静态库.lib } if (isLibrarySupported) //成功加载库的情况下，包含第三方库的头文件 { // Include path System.Console.WriteLine(\u0026#34;----- PublicIncludePaths.Add true\u0026#34;); PublicIncludePaths.Add(Path.Combine(MyLibPath, \u0026#34;Include\u0026#34;)); } return isLibrarySupported; } /*------------------End------------------*/ } 根据“签名串”构造HTTP请求，发送短信 3、接下来将上面“纯C++项目”中用来生成“签名串”的函数，写到插件中对用的蓝图函数库中，编译即可。 这里需要注意的是：使用函数“ToUnixTimestamp()”生成的时间戳和中国时区相差八个小时，计算时间戳的时候需要减去“28800” 编译后使用蓝图调用函数“SendSimpleHttpRespont1()”即可发送短信，我这里是用来测试，所以将生成签名串的逻辑放到了客户端，正式项目中不建议将这部分代码放到客户端，因为将秘钥ID和秘钥放到客户端是危险的。项目中可以采用分布式，将含有敏感的信息放到服务端。 cpp文件（部分代码）完整源代码参考压缩文件：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 void USimpleHttpBPLibrary::SendSimpleHttpRespont1() { TSharedPtr\u0026lt;IHttpRequest\u0026gt; HttpReuest = FHttpModule::Get().CreateRequest(); FDateTime Now; int64 Abs = 28800; int64 timestamp = Now.Now().ToUnixTimestamp() - Abs; //Now.UtcNow().ToString(); const FString str_Timestamp = FString::FromInt(timestamp); FString str_Authorization = MakeAuthorization(timestamp); FString Data = \u0026#34;{\\\u0026#34;PhoneNumberSet\\\u0026#34;:[\\\u0026#34;+86184\\\u0026#34;],\\\u0026#34;SmsSdkAppId\\\u0026#34;:\\\u0026#34;1402745\\\u0026#34;,\\\u0026#34;SignName\\\u0026#34;:\\\u0026#34;Imrcao\\\u0026#34;,\\\u0026#34;TemplateId\\\u0026#34;:\\\u0026#34;933779\\\u0026#34;,\\\u0026#34;TemplateParamSet\\\u0026#34;:[\\\u0026#34;564131\\\u0026#34;]}\u0026#34;; FString URL = \u0026#34;https://sms.tencentcloudapi.com/\u0026#34;; //FString URL = \u0026#34;sms.tencentcloudapi.com\u0026#34;; HttpReuest-\u0026gt;SetVerb(\u0026#34;POST\u0026#34;); HttpReuest-\u0026gt;SetHeader(\u0026#34;Content-Type\u0026#34;, \u0026#34;application/json\u0026#34;); //公共参数设置为头部 HttpReuest-\u0026gt;SetHeader(\u0026#34;X-TC-Action\u0026#34;, \u0026#34;SendSms\u0026#34;); HttpReuest-\u0026gt;SetHeader(\u0026#34;X-TC-Region\u0026#34;, \u0026#34;ap-nanjing\u0026#34;); HttpReuest-\u0026gt;SetHeader(\u0026#34;X-TC-Timestamp\u0026#34;, str_Timestamp); HttpReuest-\u0026gt;SetHeader(\u0026#34;X-TC-Version\u0026#34;, \u0026#34;2021-01-11\u0026#34;); HttpReuest-\u0026gt;SetHeader(\u0026#34;Authorization\u0026#34;, str_Authorization); //HttpReuest-\u0026gt;SetHeader(\u0026#34;Host\u0026#34;, \u0026#34;sms.tencentcloudapi.com\u0026#34;); HttpReuest-\u0026gt;SetURL(URL); HttpReuest-\u0026gt;SetContentAsString(Data); HttpReuest-\u0026gt;OnProcessRequestComplete().BindStatic(\u0026amp;USimpleHttpBPLibrary::OnRequestComplete); if (HttpReuest-\u0026gt;ProcessRequest()) { //LoginMsg(TEXT(\u0026#34;Post ok\u0026#34;)); GEngine-\u0026gt;AddOnScreenDebugMessage(-1, 5.0f, FColor::Blue, \u0026#34;Success\u0026#34;); } else { //LoginMsg(TEXT(\u0026#34;Post failed\u0026#34;)); GEngine-\u0026gt;AddOnScreenDebugMessage(-1, 5.0f, FColor::Blue, \u0026#34;Faile\u0026#34;); } } FString USimpleHttpBPLibrary::MakeAuthorization(int64 Timestamp_) { // 密钥参数 string SECRET_ID = \u0026#34;AKIDZy*********evvfCU\u0026#34;; string SECRET_KEY = \u0026#34;3pLaZH********rkNkFTQ6VdU5PS\u0026#34;; string service = \u0026#34;sms\u0026#34;; string host = \u0026#34;sms.tencentcloudapi.com\u0026#34;; string region = \u0026#34;ap-nanjing\u0026#34;; string action = \u0026#34;SendSms\u0026#34;; string version = \u0026#34;2021-01-11\u0026#34;; int64_t timestamp = Timestamp_; string date = get_data(timestamp); // ************* 步骤 1：拼接规范请求串 ************* string httpRequestMethod = \u0026#34;POST\u0026#34;; string canonicalUri = \u0026#34;/\u0026#34;; string canonicalQueryString = \u0026#34;\u0026#34;; //string canonicalHeaders = \u0026#34;content-type:application/json; charset=utf-8\\nhost:\u0026#34; + host + \u0026#34;\\n\u0026#34;; string canonicalHeaders = \u0026#34;content-type:application/json\\nhost:\u0026#34; + host + \u0026#34;\\n\u0026#34;; string signedHeaders = \u0026#34;content-type;host\u0026#34;; //string payload = \u0026#34;{\\\u0026#34;Limit\\\u0026#34;: 1, \\\u0026#34;Filters\\\u0026#34;: [{\\\u0026#34;Values\\\u0026#34;: [\\\u0026#34;\\\\u672a\\\\u547d\\\\u540d\\\u0026#34;], \\\u0026#34;Name\\\u0026#34;: \\\u0026#34;instance-name\\\u0026#34;}]}\u0026#34;; string payload = \u0026#34;{\\\u0026#34;PhoneNumberSet\\\u0026#34;:[\\\u0026#34;+8618********34\\\u0026#34;],\\\u0026#34;SmsSdkAppId\\\u0026#34;:\\\u0026#34;1400512745\\\u0026#34;,\\\u0026#34;SignName\\\u0026#34;:\\\u0026#34;Imrcao\\\u0026#34;,\\\u0026#34;TemplateId\\\u0026#34;:\\\u0026#34;933779\\\u0026#34;,\\\u0026#34;TemplateParamSet\\\u0026#34;:[\\\u0026#34;564131\\\u0026#34;]}\u0026#34;; string hashedRequestPayload = sha256Hex(payload); string canonicalRequest = httpRequestMethod + \u0026#34;\\n\u0026#34; + canonicalUri + \u0026#34;\\n\u0026#34; + canonicalQueryString + \u0026#34;\\n\u0026#34; + canonicalHeaders + \u0026#34;\\n\u0026#34; + signedHeaders + \u0026#34;\\n\u0026#34; + hashedRequestPayload; cout \u0026lt;\u0026lt; canonicalRequest \u0026lt;\u0026lt; endl; // ************* 步骤 2：拼接待签名字符串 ************* string algorithm = \u0026#34;TC3-HMAC-SHA256\u0026#34;; string RequestTimestamp = int2str(timestamp); string credentialScope = date + \u0026#34;/\u0026#34; + service + \u0026#34;/\u0026#34; + \u0026#34;tc3_request\u0026#34;; string hashedCanonicalRequest = sha256Hex(canonicalRequest); string stringToSign = algorithm + \u0026#34;\\n\u0026#34; + RequestTimestamp + \u0026#34;\\n\u0026#34; + credentialScope + \u0026#34;\\n\u0026#34; + hashedCanonicalRequest; cout \u0026lt;\u0026lt; stringToSign \u0026lt;\u0026lt; endl; // ************* 步骤 3：计算签名 *************** string kKey = \u0026#34;TC3\u0026#34; + SECRET_KEY; string kDate = HmacSha256(kKey, date); string kService = HmacSha256(kDate, service); string kSigning = HmacSha256(kService, \u0026#34;tc3_request\u0026#34;); string signature = HexEncode(HmacSha256(kSigning, stringToSign)); cout \u0026lt;\u0026lt; signature \u0026lt;\u0026lt; endl; // ************* 步骤 4：拼接 Authorization ************* string authorization = algorithm + \u0026#34; \u0026#34; + \u0026#34;Credential=\u0026#34; + SECRET_ID + \u0026#34;/\u0026#34; + credentialScope + \u0026#34;, \u0026#34; + \u0026#34;SignedHeaders=\u0026#34; + signedHeaders + \u0026#34;, \u0026#34; + \u0026#34;Signature=\u0026#34; + signature; cout \u0026lt;\u0026lt; authorization \u0026lt;\u0026lt; endl; return FString(authorization.c_str()); } \u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;分割线（更新）\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;-\n打包时出现的报错问题 1、报错如下，显示链接相关问题。先说结果，这个报错并没有实际解决。但最终还是完成了这个功能，下面讲一下思路。 UATHelper: Packaging (Windows (64-bit)): libcurl_a.lib(bio_lib.obj) : error LNK2005: BIO_free already defined in libcrypto.lib(libcrypto-1_1-x64.dll) UATHelper: Packaging (Windows (64-bit)): libcurl_a.lib(bss_mem.obj) : error LNK2005: BIO_new_mem_buf already defined in libcrypto.lib(libcrypto-1_1-x64.dll) UATHelper: Packaging (Windows (64-bit)): libcurl_a.lib(p_lib.obj) : error LNK2005: EVP_PKEY_assign already defined in libcrypto.lib(libcrypto-1_1-x64.dll) UATHelper: Packaging (Windows (64-bit)): libcurl_a.lib(p_lib.obj) : error LNK2005: EVP_PKEY_new already defined in libcrypto.lib(libcrypto-1_1-x64.dll)\n2、前面说了最好将秘钥ID和秘钥放到服务器中，所以我试着将发送验证码的逻辑放到服务器中。我这里用的是用UE4的独立程序做的分布式服务器，关于如何使用UE4开发独立程序可以参考《大象无形》这本书的第14章，至于如何做服务器就比较麻烦，可以参考AboutCG上人宅老师的课程《MOba分布式网络游戏全流程高级教学》第十章和第十一章，看这个教程需要一定的基础知识。\n3、具体实现步骤：首先将插件放到引擎的插件目录中作为引擎插件，然后修改插件部分代码，让它可以给独立程序使用。我这里的插件目录为：（注意使用的是源代码引擎） “C:\\UnrealEngine-4.25\\UnrealEngine\\Engine\\Plugins\\TencentSendSmaAPI” 需要知道的是独立程序不能引用模块“Engine”和“Editor”，所以要修改插件中“.Build.cs”文件，将“Engine”模块给删掉。如果你创建的插件是蓝图函数库，且继承了“UBlueprintFunctionLibrary”，去掉引擎模块之后，编译会报错，所以需要将其改为继承“UObject”。实例代码如下： UCLASS()\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class UTencentSendSmaAPIBPLibrary : public UBlueprintFunctionLibrary { GENERATED_UCLASS_BODY() public: UFUNCTION(BlueprintCallable, Category = \u0026#34;SimpleHttp\u0026#34;) static void TrySendVerificationBP(FString Phone); }; //改为如下： UCLASS() class UTencentSendSmaAPIBPLibrary : public UObject { GENERATED_UCLASS_BODY() public: UFUNCTION(BlueprintCallable, Category = \u0026#34;SimpleHttp\u0026#34;) static void TrySendVerificationBP(FString Phone); }; 4、在独立程序中“.Build.cs”文件中，添加插件：\nusing UnrealBuildTool;\npublic class DbServer : ModuleRules { public DbServer(ReadOnlyTargetRules Target) : base(Target) {\n1 2 3 4 5 6 7 8 9 10 11 PublicIncludePaths.Add(\u0026#34;Runtime/Launch/Public\u0026#34;); PrivateIncludePaths.Add(\u0026#34;Runtime/Launch/Private\u0026#34;); PrivateDependencyModuleNames.Add(\u0026#34;Core\u0026#34;); PrivateDependencyModuleNames.Add(\u0026#34;Projects\u0026#34;); PrivateDependencyModuleNames.Add(\u0026#34;ApplicationCore\u0026#34;) PrivateDependencyModuleNames.Add(\u0026#34;TencentSendSmaAPI\u0026#34;); //发送短信插件 PrivateDependencyModuleNames.Add(\u0026#34;HTTP\u0026#34;); } } 编译后发现还是会报错，这时修改插件中的“.Build.cs”文件，将函数”LoadThirdPartyLib”中的两行代码删除就可以编译通过了，代码如下；\npublic bool LoadThirdPartyLib(ReadOnlyTargetRules Target) {\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 bool isLibrarySupported = false; if ((Target.Platform == UnrealTargetPlatform.Win64) || (Target.Platform == UnrealTargetPlatform.Win32))//平台判断 { isLibrarySupported = true; System.Console.WriteLine(\u0026#34;----- isLibrarySupported true\u0026#34;); string LibrariesPath = Path.Combine(MyLibPath, \u0026#34;Lib\u0026#34;); //PublicAdditionalLibraries.Add(Path.Combine(LibrariesPath,/* PlatformSubPath,*/ \u0026#34;libssl.lib\u0026#34;));//加载第三方静态库.lib //PublicAdditionalLibraries.Add(Path.Combine(LibrariesPath,/* PlatformSubPath,*/ \u0026#34;libcrypto.lib\u0026#34;));//加载第三方静态库.lib } if (isLibrarySupported) //成功加载库的情况下，包含第三方库的头文件 { // Include path System.Console.WriteLine(\u0026#34;----- PublicIncludePaths.Add true\u0026#34;); PublicIncludePaths.Add(Path.Combine(MyLibPath, \u0026#34;Include\u0026#34;)); } return isLibrarySupported; } /*------------------End------------------*/ 5、最后调用发送短信的函数，代码如下。配合客户端调试没问题后即可将独立程序剥离，部署到服务器上，关于如何剥离独立程序请看文章《如何剥离UE4独立程序》。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include \u0026#34;SendSms.h\u0026#34; if (USendSms::GetSendSms()) { //先不考虑验证码发送失败的情况（手机号收入正确一般不会失败，同一个手机号在在三分钟、一小时、一天内都有接收验证数量上限） if (USendSms::GetSendSms()-\u0026gt;TrySendVerification(account)) { FString Verica = USendSms::GetSendSms()-\u0026gt;GetVerification(); if (Verica.Len() == 6) { //将验证码发送给Login服务器，在Login服务器中将验证码转发给用户客户端 SIMPLE_PROTOCOLS_SEND(SP_GetVerificationSucceed, Verica, AddrInfo); } } } ","permalink":"https://imrcao.top/posts/%E5%BD%92%E6%A1%A3/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8%E8%85%BE%E8%AE%AF%E4%BA%91%E7%9F%AD%E4%BF%A1api%E5%8F%91%E9%80%81%E9%AA%8C%E8%AF%81%E7%A0%81/","summary":"前言： （写在前面的一些废话）最近在写给手机发送短信的一个功能，之前一直在用“创蓝253短信平台”，调用起来也是非常简单，根据域名构建一个Http请求即可。但是最近这个平台对签名审核变得非常的严格且对一些平台和APP是不发的（应该是因为这些小平台的安全性没有做好，才会这么严格），申请的时候还需要 APP的下载地址、官网、设计图纸等等，付款还需要公对公，这对于我们这些独立开发者来说很不友好，我提供了公司的营业执照（一个朋友的空壳公司）和部分APP的设计图纸（做的一个交友类小游戏），申请了三四次都给我驳回了，联系客服又给我说交友类、棋牌类游戏不发（一万个草泥马）。决定还是研究腾讯的短信服务吧，但是腾讯的短信API调用起来不是一般的麻烦（由于腾讯的SDK不支持在Windows环境，所以只能硬着头皮调用API了）。下面主要记录一下在调用腾讯短信API中的一些主要步骤。\n最终目的： 使用UE4调用腾讯的发送短信API，给手机发送短信。\n介绍腾讯云短信服务及用“API Explorer\u0026quot;在线发送验证码 官方文档上有详细的文档说明，这里就不多赘述，这里主要是总结一下用法。我用的V3.0版本的API，使用 API的核心就是通过一些加密算法构建“签名串”，这个签名串用于构建HTTP请求，加密算法主要用的是“Opessll库”里面的部分函数，这个库的用法我单独写了一篇文章，参考“在Windows下配置Openssl环境”。 在测试阶段推荐使用“API Explorer”，这个可以在线发送验证码，生成签名串。发送验证码需要开通短信服务并申请签名和签名模板，还要申请腾讯SDK的秘钥和 ID（每个用户最多申请两个秘钥和 ID，）这部分官方文档有详细步骤，参考我当时发送的界面如图1-1所示。“API Explorer”生成的签名串可以用来验证你的生成签名串的程序，如图1-2所示。\n使用\u0026quot;Postman\u0026quot;测试发送Http/Https请求 在使用C++编写HTTP请求之前，可以先使用软件“Postman”发送测试请求，以确保请求串没有错误。使用方法如图1-3所示： 使用纯C++项目生成“签名串” 创建一个C++的空项目，添加一个CPP文件后，将官网的代码粘贴进去（官方文档链接：https://cloud.tencent.com/document/product/382/52072#C.2B.2B），参考文章“在Windows下配置Openssl环境”配好环境后，还需要改一个名叫“get_data”函数，如下面示例所示，之后应该就可以编译通过了。官网示例使用的接口不是发送短信接口，所以需要稍微改动一下代码。发送短信的部分改动代码如下：\n函数改动： string get_data(int64_t\u0026amp; timestamp) {\nint64_t ii = 1231232133; string utcDate; char buff[20] = { 0 }; //改动了这里，牵扯到time_t类型的初始化 time_t timenow = (time_t)(timestamp);\n1 2 3 4 5 6 7 8 9 10 11 struct tm sttime; //sttime = *gmtime(\u0026amp;timestamp); gmtime_s(\u0026amp;sttime, \u0026amp;timenow); //gmtime_s(, ); strftime(buff, sizeof(buff), \u0026#34;%Y-%m-%d\u0026#34;, \u0026amp;sttime); utcDate = string(buff); return utcDate; } SendSms接口，生成字符串所必要的一些参数： // 密钥参数 string SECRET_ID = \u0026ldquo;AKI**evvfCU\u0026rdquo;; string SECRET_KEY = \u0026ldquo;3pLaZrkNkFTQ6VdU5PS\u0026rdquo;;","title":"如何使用腾讯云短信API发送验证码"},{"content":"经本人完整踩坑之后，已经成功在windows PC上将UE4项目打包IOS。本文主要记录踩坑过程，并介绍在Windows PC上将UE4 项目打包IOS 的方法，包含配置证书、配置Mac，编译及打包等几个方面。 一般的，UE4开发主要在win平台进行，主要原因是渲染和平台支持优于Mac。到了打包时，仅蓝图的ios项目可以在window进行编译及打包（此方法本文不做说明），而C++ ios项目必须在Mac机器进行编译及打包，所以这就决定了Windows开发，Mac打包的主要路线。 那么，我们先要有台Mac机器，如果没有，那就只能先在Windows上安装Mac虚拟机。如果有，直接跳过步骤一\n一、Mac虚拟机的安装 1、下载安装VMware15.5 链接：https://pan.baidu.com/s/1JeXYaF1gX4knxOs3Me1POw 提取码：wrbf 其他虚拟机能不能安装MacOS 未知，注意使用15版本不要使用16最新版 序列码YG5H2-ANZ0H-M8ERY-TXZZZ-YKRV8 2、用Unlocker解锁 使用大佬魔改过的MK Unlocker，下载解压后（可以不放入VMware安装文件夹）右键“以管理员身份打开” win_install.cmd，让它自己进行就好了，这个版本应该不会有问题。\n此时成功解锁之后，安装虚拟机时可以有MacOS选项 下载MacOS镜像 macos 10.15下载： 下载链接：点击下载 提取码：spc4\n1 2 本人下载的是Catalina10.15.1.cdr 版本，不过后来启动之后直接来了一场更新， 可以到这里找一下较新的镜像文件。 安装虚拟机的过程不在赘述，有一点就是分配硬盘时不要分配动态空间，选择直接分配，不然后续MacOS 无法在此虚拟硬盘完成安装，具体原因不清楚。 MacOS虚拟机安装完成之后，就相当于有Mac机啦 接着马上安装Xcode，重要！\n二、证书 证书和描述文件的配置也不做太多赘述，搜索一下文章一大堆，这里主要说一下关键点。证书的配置需要在UE4中和Mac机都完成配置，配置好之后大概如下两图： 三、配置远程Mac 1、在Mac系统中，系统偏好设置-共享 打开远程登录选项，并选择所有用户 2、UE4中 项目设置-平台-IOS-构建 远程服务名处可输入远程mac登录名 或者IP地址 用户名处输入mac用户名。 点击生成SSH键，生成工程中要设置此SSH的密码，建议直接摁回车，不设置密码。 此SSH可共享给其他人，那么其他人也可以连接此远程Mac进行打包\n四、远程打包 此时，在UE4中打包项目 选择IOS 竟然打开了浏览器及UE4帮助文档。。。 1、安装Itunes 和Windows Store？ 搜索之后发现还需要在Windows中安装Itunes，据说是因为需要使用Itunes的Mac SDK? 本人之前在Itunes官网进行了下载及安装，安装完成之后发现没有用，点击打包依然打开了浏览器？ 经过多方搜索后，发现需要在Windows Store 安装Itunes？？？ UE4的这一步操作实在是不太懂。 那就照做把，然而当我卸载掉Itunes，打开了windows主菜单，发现没有Windows Store。。。 大概是因为现在大部分win10系统镜像都删除了应用商店把。 尝试多种安装Windows Store方法后，发现此方法是唯一可行的： Windows LTSC、LTSB、Server 安装 Windows Store 应用商店\n当我安装了Windows store 又在应用商店中安装了Itunes之后。 点击IOS 打包 终于开始打包了， 经过漫长的第一次编译，最终打包成功。 打包成功之后会在你的windows机器上生成一个.ipa文件，直接部署到ios项目即可。\n","permalink":"https://imrcao.top/posts/%E5%BD%92%E6%A1%A3/ue4-windows%E6%89%93%E5%8C%85ios-%E8%B8%A9%E5%9D%91%E8%AE%B0%E5%BD%95/","summary":"经本人完整踩坑之后，已经成功在windows PC上将UE4项目打包IOS。本文主要记录踩坑过程，并介绍在Windows PC上将UE4 项目打包IOS 的方法，包含配置证书、配置Mac，编译及打包等几个方面。 一般的，UE4开发主要在win平台进行，主要原因是渲染和平台支持优于Mac。到了打包时，仅蓝图的ios项目可以在window进行编译及打包（此方法本文不做说明），而C++ ios项目必须在Mac机器进行编译及打包，所以这就决定了Windows开发，Mac打包的主要路线。 那么，我们先要有台Mac机器，如果没有，那就只能先在Windows上安装Mac虚拟机。如果有，直接跳过步骤一\n一、Mac虚拟机的安装 1、下载安装VMware15.5 链接：https://pan.baidu.com/s/1JeXYaF1gX4knxOs3Me1POw 提取码：wrbf 其他虚拟机能不能安装MacOS 未知，注意使用15版本不要使用16最新版 序列码YG5H2-ANZ0H-M8ERY-TXZZZ-YKRV8 2、用Unlocker解锁 使用大佬魔改过的MK Unlocker，下载解压后（可以不放入VMware安装文件夹）右键“以管理员身份打开” win_install.cmd，让它自己进行就好了，这个版本应该不会有问题。\n此时成功解锁之后，安装虚拟机时可以有MacOS选项 下载MacOS镜像 macos 10.15下载： 下载链接：点击下载 提取码：spc4\n1 2 本人下载的是Catalina10.15.1.cdr 版本，不过后来启动之后直接来了一场更新， 可以到这里找一下较新的镜像文件。 安装虚拟机的过程不在赘述，有一点就是分配硬盘时不要分配动态空间，选择直接分配，不然后续MacOS 无法在此虚拟硬盘完成安装，具体原因不清楚。 MacOS虚拟机安装完成之后，就相当于有Mac机啦 接着马上安装Xcode，重要！\n二、证书 证书和描述文件的配置也不做太多赘述，搜索一下文章一大堆，这里主要说一下关键点。证书的配置需要在UE4中和Mac机都完成配置，配置好之后大概如下两图： 三、配置远程Mac 1、在Mac系统中，系统偏好设置-共享 打开远程登录选项，并选择所有用户 2、UE4中 项目设置-平台-IOS-构建 远程服务名处可输入远程mac登录名 或者IP地址 用户名处输入mac用户名。 点击生成SSH键，生成工程中要设置此SSH的密码，建议直接摁回车，不设置密码。 此SSH可共享给其他人，那么其他人也可以连接此远程Mac进行打包\n四、远程打包 此时，在UE4中打包项目 选择IOS 竟然打开了浏览器及UE4帮助文档。。。 1、安装Itunes 和Windows Store？ 搜索之后发现还需要在Windows中安装Itunes，据说是因为需要使用Itunes的Mac SDK? 本人之前在Itunes官网进行了下载及安装，安装完成之后发现没有用，点击打包依然打开了浏览器？ 经过多方搜索后，发现需要在Windows Store 安装Itunes？？？ UE4的这一步操作实在是不太懂。 那就照做把，然而当我卸载掉Itunes，打开了windows主菜单，发现没有Windows Store。。。 大概是因为现在大部分win10系统镜像都删除了应用商店把。 尝试多种安装Windows Store方法后，发现此方法是唯一可行的： Windows LTSC、LTSB、Server 安装 Windows Store 应用商店","title":"UE4 windows打包IOS 踩坑记录"},{"content":"前言：UE4-427版本在虚拟拍摄领域迎来了一次大更新，各种工具趋于完善，现场的效率越来越高，其中一项更新就是LiveLinkVRPN插件的引入，之前版本是通过在“配置文件”中读取跟踪设备数据的（例如HTC），现在可以通过这个插件直接获取数据，VRPN服务器还是要一直开着的。 注意事项：（需要打开SteamVR、LivelinkVRPN插件、VRPN服务器）\n1、LivelinkVRPN 427中如何使用Vive手柄进行输入 在配置文件中移除了使用VRPN的输入配置，用livelike代替，使用方法： （1）、首先打开SteamVR和VRPN服务器，\n（2）、打开UE4编辑器，打开livelike,如图所示添加一个Livelink源，如图所示\nIPAddress \u0026ndash; 输入IP地址（需要注意的是后面要加“:3884”端口号）\nDevice Name \u0026ndash; 输入设备名称（VRPN服务器上的名称，如上图红线所示）\nSubject Name\u0026ndash;源名称（ 随便填写）\nType \u0026ndash; 源类型（使用Vive的话，就选Tracker）\n（3）、选择刚刚创建的Livelink源，添加一个“Pre Processors”，需要注意的是Livelink的三个轴向。如图所示：\n（4）（注意）如果需要在Ndisplay模式下运行的话，需要开启“SteamVR”插件，不然使用SwitchBoard启动的时候，SteamVR会产生崩溃，导致VIVE手柄的位置信息不能进入UE。\n（5）、在全部设置好后，将LiveLink设置保存为一个模板，并在项目设置中指定此模板，这样才能在下次启动工程的时候自动接收livelink数据。如图所示： ","permalink":"https://imrcao.top/posts/%E5%BD%92%E6%A1%A3/ue_427%E6%9B%B4%E6%96%B0%E6%97%A5%E5%BF%97%E4%B9%8Blivelinkvrpnhtcvive/","summary":"前言：UE4-427版本在虚拟拍摄领域迎来了一次大更新，各种工具趋于完善，现场的效率越来越高，其中一项更新就是LiveLinkVRPN插件的引入，之前版本是通过在“配置文件”中读取跟踪设备数据的（例如HTC），现在可以通过这个插件直接获取数据，VRPN服务器还是要一直开着的。 注意事项：（需要打开SteamVR、LivelinkVRPN插件、VRPN服务器）\n1、LivelinkVRPN 427中如何使用Vive手柄进行输入 在配置文件中移除了使用VRPN的输入配置，用livelike代替，使用方法： （1）、首先打开SteamVR和VRPN服务器，\n（2）、打开UE4编辑器，打开livelike,如图所示添加一个Livelink源，如图所示\nIPAddress \u0026ndash; 输入IP地址（需要注意的是后面要加“:3884”端口号）\nDevice Name \u0026ndash; 输入设备名称（VRPN服务器上的名称，如上图红线所示）\nSubject Name\u0026ndash;源名称（ 随便填写）\nType \u0026ndash; 源类型（使用Vive的话，就选Tracker）\n（3）、选择刚刚创建的Livelink源，添加一个“Pre Processors”，需要注意的是Livelink的三个轴向。如图所示：\n（4）（注意）如果需要在Ndisplay模式下运行的话，需要开启“SteamVR”插件，不然使用SwitchBoard启动的时候，SteamVR会产生崩溃，导致VIVE手柄的位置信息不能进入UE。\n（5）、在全部设置好后，将LiveLink设置保存为一个模板，并在项目设置中指定此模板，这样才能在下次启动工程的时候自动接收livelink数据。如图所示： ","title":"更新日志之LivelinkVRPN（HTCVive）"},{"content":"前言 最近在做一个直播中用到的小工具，需求是从斗鱼或者虎牙平台中拉去直播的数据（弹幕和礼物等等），并将这些数据实时推流到一个UE4客户端，在客户端中根据这些数据做一些业务逻辑。这里采用的模型是，使用python写数据推流服务器，UE4作为客户端持续接收数据。这里主要记录一下整个过程中遇到的问题。\n为什么是WebSocket https://www.ruanyifeng.com/blog/2017/05/websocket.html\nPIP（python包管理工具） https://baike.baidu.com/item/PIP/20435212?fr=aladdin\npython配置环境变量 https://www.cnblogs.com/huangbiquan/p/7784533.html\n在cmd中安装Python工具包（webSocket） https://jingyan.baidu.com/article/86f4a73ea7766e37d7526979.html\n在PyCharm中安装Python工具包 https://blog.csdn.net/m0_37332816/article/details/81353659\nhttps://zhuanlan.zhihu.com/p/314507897\n","permalink":"https://imrcao.top/posts/%E5%BD%92%E6%A1%A3/python%E5%92%8Cue4%E7%BB%93%E5%90%88%E5%BB%BA%E7%AB%8Bwebsocket%E9%80%9A%E4%BF%A1/","summary":"前言 最近在做一个直播中用到的小工具，需求是从斗鱼或者虎牙平台中拉去直播的数据（弹幕和礼物等等），并将这些数据实时推流到一个UE4客户端，在客户端中根据这些数据做一些业务逻辑。这里采用的模型是，使用python写数据推流服务器，UE4作为客户端持续接收数据。这里主要记录一下整个过程中遇到的问题。\n为什么是WebSocket https://www.ruanyifeng.com/blog/2017/05/websocket.html\nPIP（python包管理工具） https://baike.baidu.com/item/PIP/20435212?fr=aladdin\npython配置环境变量 https://www.cnblogs.com/huangbiquan/p/7784533.html\n在cmd中安装Python工具包（webSocket） https://jingyan.baidu.com/article/86f4a73ea7766e37d7526979.html\n在PyCharm中安装Python工具包 https://blog.csdn.net/m0_37332816/article/details/81353659\nhttps://zhuanlan.zhihu.com/p/314507897","title":"Python和UE4结合建立WebSocket通信"},{"content":"前言：最近虚幻引擎更新到427版本，在影视方面更新了很多的重要的内容，特别是Ndisplay-LED（InVFXCamea）拍摄这一块，用官方的话说，这个版本的Ndisplay是可以用于生产的版本。更新的内容的深度和广度都很大，导致之前的低版本功能无法直接升级到427版本。下面来看一下如何使用新版本的Ndisplay功能。\n一、创建工程，部署环境 选择Ndisplay模板，创建一个空的工程，如图1-1所示。\n图1-1\n选在并打开配置文件“NDC_Basic”，选择“Node_0(Master)”，在细节面板中更改Window的尺寸大小（根据自己显示器分辨率进行调整）；选择“VP_0”，调整投影策略“Projection Policy”，并调整视口大小。如图1-2所示。\n图1-2\n打开SwitchboardListener，然后再打开Switchboard，如图1-3所示。首次打开会提示安装相关环境，等待安装完毕即可。 （注意）有的时候Switchboard界面弹不出来（点击任务栏图标的时候，总是出现纯白色预览缩略图，就是点出界面，这里无法好像无法截图，只能描述。应该是个bug，后面可能会修复吧），在纯白缩略图上单击右键，选择最大化即可。如图1-4所示。\n图1-3\n图1-4\n创建配置文件：现在打开了Switchboard的监听和客户端，首次打开创建一个新的配置文件，如图1-5所示。配置文件界面如图1-6所示： (注意)ConfigPath是配置文件存储的路径，注意界面上只会显示配置文件名称，不会显示路径，初始应该是默认路径，我这里的完整路径是“D:\\Program Files\\Epic Games\\UE_4.27\\Engine\\Plugins\\VirtualProduction\\Switchboard\\Source\\Switchboard\\configs” Uproject和Engine DIr就常规填写项目位置和引擎位置即可。点击ok创建配置文件，如图1-5所示。\n图1-5\n图1-6\n添加Ndisplay节点，点击“AddDevice”选择“Ndisplay”类型，在弹出来的界面上点击“Browse”，选择模板自带的配置文件。如图1-7所示。\n图1-7\n添加Unreal项目，用于多用户编辑，填写IP地址后点击ok，这里IP填127.0.0.1。如图1-8所示。\n图1-8\n点击“Settings”，在MultiUserServer一栏下，将AutoJoin勾选上。如图1-9所示\n图1-9\n8、首先启动Ndisplay节点，先点击链接（1），再点击启动（2）（在启动之前记得将“SteamVR”插件勾选上，不然会造成SreamVR软件的崩溃，如图1-10所示）。在启动Unreal编辑器之前，将Settings-\u0026gt;MultiUserServe-\u0026gt;AutoLunch勾选掉，不用启动Server服务器了（上一次启动过了）。在UnrealDevice下先点击连接（3），再点击启动（4）。如图1-11所示\n图1-10\n图1-11\n到这里就完成了在单个机器上启动Ndisplay和多用户编辑。在编辑器中做出更改，Ndisplay视口中会发生实时的变化。\n二、添加InnerVFXCamera(内视锥) 向场景中拖入一个“CineCameraActor”,并将它作为Ndis配置文件“Monitor_1Host_1Node”的子节点（我这里将Ndis配置文件改了名字）。选中CineCameraActor，在细节面板下添加一个“LiveLinkComponentController”，并在属性下指定自己的LiveLink。如图所示2-1所示\n图2-1\n打开Ndisplay配置文件，添加组件“ICVFXCamera”，\n回到世界大纲，选中配置文件，在细节面板中指定刚才拖入的CinCameraActor即可。\n关于如何使用Livelink接收数据，请参考文章“” 现在保存退出即可。 根据上述步骤使用Switchboard启动，就可以根据HTCVive手柄驱动内视锥了。\nLivelinkVRPN 前言：UE4-427版本在虚拟拍摄领域迎来了一次大更新，各种工具趋于完善，现场的效率越来越高，其中一项更新就是LiveLinkVRPN插件的引入，之前版本是通过在“配置文件”中读取跟踪设备数据的（例如HTC），现在可以通过这个插件直接获取数据，VRPN服务器还是要一直开着的。 注意事项：（需要打开SteamVR、LivelinkVRPN插件、VRPN服务器） 427中如何使用Vive手柄进行输入 在配置文件中移除了使用VRPN的输入配置，用livelike代替，使用方法： （1）、首先打开SteamVR和VRPN服务器，\n（2）、打开UE4编辑器，打开livelike,如图所示添加一个Livelink源，如图所示\nIPAddress \u0026ndash; 输入IP地址（需要注意的是后面要加“:3884”端口号）\nDevice Name \u0026ndash; 输入设备名称（VRPN服务器上的名称，如上图红线所示）\nSubject Name\u0026ndash;源名称（ 随便填写）\nType \u0026ndash; 源类型（使用Vive的话，就选Tracker）\n（3）、选择刚刚创建的Livelink源，添加一个“Pre Processors”，需要注意的是Livelink的三个轴向。如图所示：\n（4）（注意）如果需要在Ndisplay模式下运行的话，需要开启“SteamVR”插件，不然使用SwitchBoard启动的时候，SteamVR会产生崩溃，导致VIVE手柄的位置信息不能进入UE。\n（5）、在全部设置好后，将LiveLink设置保存为一个模板，并在项目设置中指定此模板，这样才能在下次启动工程的时候自动接收livelink数据。如图所示： IPad控制\n1、首先在Content下创建“Remote Control Preset”资产，在“Miscellaneous”下找到此项，创建并命名，如图4-1所示 图4-1\n2、双击打开远程控制资源类，在右上角找到“EditMode”打开编辑模式，如图4-2所示，此模式打开后，在世界大纲中选中需要被控制的Actor，选中Actor后就可以在Detail面板中看到各个属性前面有一个小眼睛，选择需要控制的属性，点击小眼睛图标即可，如图4-4所示。 图4-2\n3、在图4-2上点击设置图标进入Remote Control设置页面，勾选“Auto Start Web Server”，即项目启动时运行WebServer。如图4-3所示。 图4-3\n4、在Ndisplay项目中一般会控制主相机和一些环境资产，如后处理、太阳光、天光等。 图4-4 5、在图4-2右上角有个启动按钮，点击即可打开Web控制页面，在此页面可自定义布局，设计各个属性的排布，webserver自带有各种集成好的控件，直接可以拿来用，如图4-5所示。 图4-5\n6、设置完毕后，即可在移动端中运行，随便找一个浏览器，将网址“http://127.0.0.1:7000/”输入到浏览器中即可运行，前提是移动端设备和服务器设备在同一个网络中，注意检查服务器的防火墙情况，一般是需要将服务器防火墙关闭的。客户端不限制平台，在安卓和苹果Ipad都是可以运行的。\nTakeReconrder录制 1、在Ndisplay中录制可以从两个地方启动录制，一个是在Switchborad上，前提是需要在Switchboard上启动项目，然后将场、镜、次的名称输入，后缀会自动递增，设置完毕后，点击右上角的红色录制按钮即可，如图5-1所示（前提是需要在项目中进行一些设置，下面会讲） 图5-1\n2、还有一种是直接在TakeRecorder界面中进行设置录制，在编辑中按如图“5-2”所示打开takeRecorder窗口，首先选在你需要录制的Actor，如图5-2所示， 图5-1\n3、在Ndisplay项目录制中，需要选择主相机或者它的父类。如图5-2所示。 图5-2\n4、选在好Actor后，点击右上角设置按钮进行一些必要的设置，其实只要录制Transform就可以了，其它的可以勾选掉。在Ndisplay群集节点中也没有必要将每一台客户端都录制，只需要录制触发录制的那一台机器即可。点击右上角红色录制按钮即可开始录制。 图5-3\n5、录制好的文件会在content文件夹下，录制的文件是Sequence,点击打开Sequence，选中录制的Actor选择“export”就可以到处FBX了。可以在maya中预览录制的动画文件。如图5-4和图5-5所示。 图5-4\n图5-5\n青瞳的使用 硬件设备： 动捕摄像头若干个，扫场用的T型杆，用于定原点和正方向的L型杆（三角形状），加密狗U盘，交换机A。 （在427中青瞳也是走的VRPN协议） 软件设备： 青瞳对应软件的安装包和UE的插件，还有一个License证书。 （软件和硬件设备供应商都会提供）\n使用步骤：\n1、首先安装动捕摄像头，动捕摄像头的焦点和焦距一般由厂家设置好的，不用手动调整，安装前需要检查摄像头的前的旋转按钮是否松动，如若松动应当拧紧。将各个摄像头用网线与交换机连接起来。（注意：交换机上需要将模式调整为标椎交换，23-24网口（就是黄色字体的网口）不能使用，如图1-1所示）\n如图1-1\n2、接下来配置PC环境，我这里使用的是多台电脑联机渲染画面（用UE4的Ndisplay将画面投到LED上，就是最近流行的虚拟制片）。所以用到了第二台交换机B（用一台交换机也可以，由于我的电脑只有一个网口所以就使用了两个交换机）。使用交换机B将各个电脑连到同一个局域网内，再使用一根网线将两个交换机直连，这样动捕摄像头的数据就会进入主PC，主PC获取的动捕数据再传入到其他PC内。\n3、配置软件环境：首先安装动捕对应的软件CMTracker（安装过程中确保加密狗插在电脑上；安装步骤如图3-1，没什么要注意的直接点下一步），安装后好会有两个程序，一个为CMTracker_Client和CMTracker_Server。然后在安装目录的Server目录下找到“CMGigEConfigurator”程序（如图3-2），运行程序后，点击ForceAll自动获取全部相机的IP（如图3-3）。 最后替换License，初次使用会有两个证书先关的文件，分别为“license.au”和“license.RyArmUdp”将这两个文件放到安装目录下的Server-\u0026gt;Licence目录下即可（如图3-4所示）。\n3-1\n3-2\n3-3\n3-2\n4、系统初始化：先连接“server”， →再打开“client”。确认系统加载的相机个数是否正确，点击“相机 视图”按钮。如图4-1\n4-1\n5、调整捕捉区域：开始调整捕捉区域，调整摄像头的方向直到能够看到捕捉区域的中心点，有时候现场太暗看不到摄像头画面，这时候可以时候用可以将T型杆或者三角形Tracker放到你要捕捉区域的中心点，这样摄像头就能够看到Tracker了，根据Tracker的位置调整捕捉区域。\n6、屏蔽环境光，校准相机捕捉图像：在“追踪”栏目下点击“开始按钮”。此时“开始按钮”变成“停止按钮”，等三秒后点击“停止”按钮。然后点击文件-保存配置。如图6-1；先在“校准设置”栏目下，确认捕捉数量，再点击“开始”按钮，在追踪环境里挥动“T型杆”，让相机搜集图片。如图6-2（建议：在整个追踪环境里来回均匀的挥动“T型校准杆”，以使校准效果达到最佳）。相机图片搜集进度条加载完成后自动计算相机的相对位置。完成后保存配置。 6-1\n6-2\n7、设置坐标系：设置坐标系的前提条件（1）、没有设置坐标系；（2）、需要修改场景里的坐标系。修改方法：修改时，先点击“开始”按钮，然后将“L型杆”放到你定义的原点位置（系统规定：X轴为右方向，Y轴为正方向，就是短的一端朝正方向），放置好后点击“设置”按钮。保存配置。如图7-1\n7-1（这张图比较清晰） 8、添加刚体“Body”：在“标记体栏”目下，先确定添加的方式为“手动”，（图标的下拉键头可改变添加方式）将要添加的body放到追踪环境里面，等到marker球比较稳定时（抖动的幅度很小），按“空格键”（PAUSE状态），框选要添加的点。然后点击“手动”，添加完成，按“空格键”继续追踪。点击”文件”，保存配置。此时需要记住刚体的ID，后面再UE中会用到。如图8-1\n8-1\n9、重置正方向：如果你自己制作的跟踪器正方向不对，可以在这里重置正方向。 如图9-1\n9-1\n10、动捕环境设置好后，就可以使用青瞳对应UE的插件拿到动捕数据了。首先将插件放到项目根目录的“Plugins”文件夹下，并启用。在任意类中添加ChingMU组件，调用蓝图节点“GetTrackerpose”就能拿到跟踪器的位置信息了，如图10-2.(输入正确的IP地址和TrackerID)\n10-1\n","permalink":"https://imrcao.top/posts/%E5%BD%92%E6%A1%A3/ue427%E7%89%88%E6%9C%AC-ndisplay%E5%BF%AB%E9%80%9F%E5%85%A5%E9%97%A8/","summary":"前言：最近虚幻引擎更新到427版本，在影视方面更新了很多的重要的内容，特别是Ndisplay-LED（InVFXCamea）拍摄这一块，用官方的话说，这个版本的Ndisplay是可以用于生产的版本。更新的内容的深度和广度都很大，导致之前的低版本功能无法直接升级到427版本。下面来看一下如何使用新版本的Ndisplay功能。\n一、创建工程，部署环境 选择Ndisplay模板，创建一个空的工程，如图1-1所示。\n图1-1\n选在并打开配置文件“NDC_Basic”，选择“Node_0(Master)”，在细节面板中更改Window的尺寸大小（根据自己显示器分辨率进行调整）；选择“VP_0”，调整投影策略“Projection Policy”，并调整视口大小。如图1-2所示。\n图1-2\n打开SwitchboardListener，然后再打开Switchboard，如图1-3所示。首次打开会提示安装相关环境，等待安装完毕即可。 （注意）有的时候Switchboard界面弹不出来（点击任务栏图标的时候，总是出现纯白色预览缩略图，就是点出界面，这里无法好像无法截图，只能描述。应该是个bug，后面可能会修复吧），在纯白缩略图上单击右键，选择最大化即可。如图1-4所示。\n图1-3\n图1-4\n创建配置文件：现在打开了Switchboard的监听和客户端，首次打开创建一个新的配置文件，如图1-5所示。配置文件界面如图1-6所示： (注意)ConfigPath是配置文件存储的路径，注意界面上只会显示配置文件名称，不会显示路径，初始应该是默认路径，我这里的完整路径是“D:\\Program Files\\Epic Games\\UE_4.27\\Engine\\Plugins\\VirtualProduction\\Switchboard\\Source\\Switchboard\\configs” Uproject和Engine DIr就常规填写项目位置和引擎位置即可。点击ok创建配置文件，如图1-5所示。\n图1-5\n图1-6\n添加Ndisplay节点，点击“AddDevice”选择“Ndisplay”类型，在弹出来的界面上点击“Browse”，选择模板自带的配置文件。如图1-7所示。\n图1-7\n添加Unreal项目，用于多用户编辑，填写IP地址后点击ok，这里IP填127.0.0.1。如图1-8所示。\n图1-8\n点击“Settings”，在MultiUserServer一栏下，将AutoJoin勾选上。如图1-9所示\n图1-9\n8、首先启动Ndisplay节点，先点击链接（1），再点击启动（2）（在启动之前记得将“SteamVR”插件勾选上，不然会造成SreamVR软件的崩溃，如图1-10所示）。在启动Unreal编辑器之前，将Settings-\u0026gt;MultiUserServe-\u0026gt;AutoLunch勾选掉，不用启动Server服务器了（上一次启动过了）。在UnrealDevice下先点击连接（3），再点击启动（4）。如图1-11所示\n图1-10\n图1-11\n到这里就完成了在单个机器上启动Ndisplay和多用户编辑。在编辑器中做出更改，Ndisplay视口中会发生实时的变化。\n二、添加InnerVFXCamera(内视锥) 向场景中拖入一个“CineCameraActor”,并将它作为Ndis配置文件“Monitor_1Host_1Node”的子节点（我这里将Ndis配置文件改了名字）。选中CineCameraActor，在细节面板下添加一个“LiveLinkComponentController”，并在属性下指定自己的LiveLink。如图所示2-1所示\n图2-1\n打开Ndisplay配置文件，添加组件“ICVFXCamera”，\n回到世界大纲，选中配置文件，在细节面板中指定刚才拖入的CinCameraActor即可。\n关于如何使用Livelink接收数据，请参考文章“” 现在保存退出即可。 根据上述步骤使用Switchboard启动，就可以根据HTCVive手柄驱动内视锥了。\nLivelinkVRPN 前言：UE4-427版本在虚拟拍摄领域迎来了一次大更新，各种工具趋于完善，现场的效率越来越高，其中一项更新就是LiveLinkVRPN插件的引入，之前版本是通过在“配置文件”中读取跟踪设备数据的（例如HTC），现在可以通过这个插件直接获取数据，VRPN服务器还是要一直开着的。 注意事项：（需要打开SteamVR、LivelinkVRPN插件、VRPN服务器） 427中如何使用Vive手柄进行输入 在配置文件中移除了使用VRPN的输入配置，用livelike代替，使用方法： （1）、首先打开SteamVR和VRPN服务器，\n（2）、打开UE4编辑器，打开livelike,如图所示添加一个Livelink源，如图所示\nIPAddress \u0026ndash; 输入IP地址（需要注意的是后面要加“:3884”端口号）\nDevice Name \u0026ndash; 输入设备名称（VRPN服务器上的名称，如上图红线所示）\nSubject Name\u0026ndash;源名称（ 随便填写）\nType \u0026ndash; 源类型（使用Vive的话，就选Tracker）\n（3）、选择刚刚创建的Livelink源，添加一个“Pre Processors”，需要注意的是Livelink的三个轴向。如图所示：\n（4）（注意）如果需要在Ndisplay模式下运行的话，需要开启“SteamVR”插件，不然使用SwitchBoard启动的时候，SteamVR会产生崩溃，导致VIVE手柄的位置信息不能进入UE。\n（5）、在全部设置好后，将LiveLink设置保存为一个模板，并在项目设置中指定此模板，这样才能在下次启动工程的时候自动接收livelink数据。如图所示： IPad控制\n1、首先在Content下创建“Remote Control Preset”资产，在“Miscellaneous”下找到此项，创建并命名，如图4-1所示 图4-1\n2、双击打开远程控制资源类，在右上角找到“EditMode”打开编辑模式，如图4-2所示，此模式打开后，在世界大纲中选中需要被控制的Actor，选中Actor后就可以在Detail面板中看到各个属性前面有一个小眼睛，选择需要控制的属性，点击小眼睛图标即可，如图4-4所示。 图4-2\n3、在图4-2上点击设置图标进入Remote Control设置页面，勾选“Auto Start Web Server”，即项目启动时运行WebServer。如图4-3所示。 图4-3","title":"202110-UE427版本-Ndisplay快速入门"},{"content":"前言： SSL是Secure Sockets Layer（安全套接层协议）的缩写，可以在Internet上提供秘密性传输。库中包含很多加密函数，可以对数据进行加密。Openssl整个软件包主要包含三部分：SSL协议库、应用程序、密码算法库。下面主要介绍如何在Windows环境下下载安装Openssl，并配置环境，用C++调用其中的部分函数对数据进行加密。测试案例是：生成一个签名串，这个签名串是腾讯云短信服务中“发送短信”API中需要的必要参数。\n下载安装： 下载地址：https://slproweb.com/products/Win32OpenSSL.html，我这里选择的版本是“Win64 OpenSSL v1.1.1L”选择了“EXE”格式，如果你的操作系统是32位或者你的项目是32位的话，就选择下载32位的安装包。安装过程比较简单直接点击下一步即可。\n配置Windows环境： 配置环境变量：右键单击“我的电脑”-\u0026gt;“属性”-\u0026gt;\u0026ldquo;高级系统设置\u0026rdquo;，在弹窗中找到“高级”-\u0026gt;\u0026ldquo;环境变量\u0026rdquo;，新建环境变量，变量名“Path”，变量值为“C:\\Program Files\\OpenSSL-Win64\\bin”，变量值为安装目录；如图1-1所示； 图1-1\n配置VS项目环境： 1、如果要在VS项目中使用SSL库，还需要在VS项目中进行相关设置。我这里使用的是VS2019创建的一个空的C++项目；添加一个“.cpp”文件“Source.cpp”，加入头文件“#include \u0026lt;openssl/sha.h\u0026gt;”和“#include \u0026lt;openssl/hmac.h\u0026gt;”，编译会报错并提示找不到头文件。 2、项目设置：项目-单击右键-属性，在属性面板中找到“VC++ Directories”-\u0026gt;\u0026ldquo;Include Directories(包含目录)\u0026quot;,添加新的目录“C:\\Program Files\\OpenSSL-Win64\\include”；同样在库目录“Library Directories”下也添加一个新的目录“C:\\Program Files\\OpenSSL-Win64\\lib”。如图1-2所示。最后配置静态链接库：选择连接器“Linker”-\u0026gt;输入“Input”-\u0026gt;添加依赖项“Additional Dependercies” 将“libssl.lib、libcrypto.lib”粘贴到空白处，点击确定即可。如图1-3所示。现在再编译项目即可编译通过。到此Openssl的环境配置就完成了，可以尝试调用一下其中的函数，后面会通过一个案例来使用Openssl库。\n图1-2\n图1-3\n在UE4C++项目中使用 在 UE4项目中配置Openssl环境（UE4中引用第三方链接库）。 在UE4项目中无法按照文章“在Windows下配置Openssl环境”配置相关环境，UE4的项目属性中没有“链接器-\u0026gt;输入”，所以无法指定第三方静态链接库。UE4引用第三方静态链接库需要在“.Build.cs”文件中配置。 首先在插件的根目录的下创建文件夹“ThirdParty”，然后去“Openssl”的安装目录下找到文件夹“OpenSSL-Win64”（我这里的目录为“C:\\Program Files\\OpenSSL-Win64”），最后将这个文件夹复制到“ThirdParty”中，并保留文件夹“Include”、“Lib”两个文件夹，其余全删除。如图1-4所示。\n图1-4\n在VS工程模块中的“.build.cs”文件中加载第三方静态链接库，代码如下所示。 // Some copyright should be here\u0026hellip; using System.IO; using UnrealBuildTool;\npublic class SimpleHttp : ModuleRules { /\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;Begin\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;/ private string ModulePath { // get { return Path.GetDirectoryName(RulesCompiler.GetModuleFilename(this.GetType().Name)); } get { return ModuleDirectory; } }\nprivate string ThirdPartyPath { get { return Path.GetFullPath(Path.Combine(ModulePath, \u0026ldquo;../../ThirdParty/\u0026rdquo;)); } } private string MyLibPath //第三方库MyTestLib的目录 { get { return Path.GetFullPath(Path.Combine(ThirdPartyPath, \u0026ldquo;OpenSSL-Win64\u0026rdquo;)); } } /\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;End\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;/\npublic SimpleHttp(ReadOnlyTargetRules Target) : base(Target) { PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 PublicIncludePaths.AddRange( new string[] { // ... add public include paths required here ... } ); PrivateIncludePaths.AddRange( new string[] { // ... add other private include paths required here ... } ); PublicDependencyModuleNames.AddRange( new string[] { \u0026#34;Core\u0026#34;, // ... add other public dependencies that you statically link with here ... } ); PrivateDependencyModuleNames.AddRange( new string[] { \u0026#34;CoreUObject\u0026#34;, \u0026#34;Engine\u0026#34;, \u0026#34;Slate\u0026#34;, \u0026#34;SlateCore\u0026#34;, \u0026#34;HTTP\u0026#34;, // ... add private dependencies that you statically link with here ... } ); DynamicallyLoadedModuleNames.AddRange( new string[] { // ... add any modules that your module loads dynamically here ... } ); /*------------------Begin------------------*/ LoadThirdPartyLib(Target); /*------------------End------------------*/ } /*------------------Begin------------------*/ public bool LoadThirdPartyLib(ReadOnlyTargetRules Target) { bool isLibrarySupported = false; if ((Target.Platform == UnrealTargetPlatform.Win64) || (Target.Platform == UnrealTargetPlatform.Win32))//平台判断 { isLibrarySupported = true; System.Console.WriteLine(\u0026#34;----- isLibrarySupported true\u0026#34;); //string PlatformSubPath = (Target.Platform == UnrealTargetPlatform.Win64) ? \u0026#34;Win64\u0026#34; : \u0026#34;Win32\u0026#34;; string LibrariesPath = Path.Combine(MyLibPath, \u0026#34;Lib\u0026#34;); PublicAdditionalLibraries.Add(Path.Combine(LibrariesPath,/* PlatformSubPath,*/ \u0026#34;libssl.lib\u0026#34;));//加载第三方静态库.lib PublicAdditionalLibraries.Add(Path.Combine(LibrariesPath,/* PlatformSubPath,*/ \u0026#34;libcrypto.lib\u0026#34;));//加载第三方静态库.lib } if (isLibrarySupported) //成功加载库的情况下，包含第三方库的头文件 { // Include path System.Console.WriteLine(\u0026#34;----- PublicIncludePaths.Add true\u0026#34;); PublicIncludePaths.Add(Path.Combine(MyLibPath, \u0026#34;Include\u0026#34;)); } return isLibrarySupported; } /*------------------End------------------*/ } 然后编译会报错显示“UI”定义重复了，根据报错打开Openssl库中对应的头文件“ossl_Type.h”，在第144行处： 由 typedef struct ui_st UI; 改为（可以改为任意名） typedef struct ui_st UI_L;\n改完后重新编译即可。\n","permalink":"https://imrcao.top/posts/%E5%BD%92%E6%A1%A3/%E5%9C%A8windows%E4%B8%8B%E9%85%8D%E7%BD%AEopenssl%E7%8E%AF%E5%A2%83/","summary":"前言： SSL是Secure Sockets Layer（安全套接层协议）的缩写，可以在Internet上提供秘密性传输。库中包含很多加密函数，可以对数据进行加密。Openssl整个软件包主要包含三部分：SSL协议库、应用程序、密码算法库。下面主要介绍如何在Windows环境下下载安装Openssl，并配置环境，用C++调用其中的部分函数对数据进行加密。测试案例是：生成一个签名串，这个签名串是腾讯云短信服务中“发送短信”API中需要的必要参数。\n下载安装： 下载地址：https://slproweb.com/products/Win32OpenSSL.html，我这里选择的版本是“Win64 OpenSSL v1.1.1L”选择了“EXE”格式，如果你的操作系统是32位或者你的项目是32位的话，就选择下载32位的安装包。安装过程比较简单直接点击下一步即可。\n配置Windows环境： 配置环境变量：右键单击“我的电脑”-\u0026gt;“属性”-\u0026gt;\u0026ldquo;高级系统设置\u0026rdquo;，在弹窗中找到“高级”-\u0026gt;\u0026ldquo;环境变量\u0026rdquo;，新建环境变量，变量名“Path”，变量值为“C:\\Program Files\\OpenSSL-Win64\\bin”，变量值为安装目录；如图1-1所示； 图1-1\n配置VS项目环境： 1、如果要在VS项目中使用SSL库，还需要在VS项目中进行相关设置。我这里使用的是VS2019创建的一个空的C++项目；添加一个“.cpp”文件“Source.cpp”，加入头文件“#include \u0026lt;openssl/sha.h\u0026gt;”和“#include \u0026lt;openssl/hmac.h\u0026gt;”，编译会报错并提示找不到头文件。 2、项目设置：项目-单击右键-属性，在属性面板中找到“VC++ Directories”-\u0026gt;\u0026ldquo;Include Directories(包含目录)\u0026quot;,添加新的目录“C:\\Program Files\\OpenSSL-Win64\\include”；同样在库目录“Library Directories”下也添加一个新的目录“C:\\Program Files\\OpenSSL-Win64\\lib”。如图1-2所示。最后配置静态链接库：选择连接器“Linker”-\u0026gt;输入“Input”-\u0026gt;添加依赖项“Additional Dependercies” 将“libssl.lib、libcrypto.lib”粘贴到空白处，点击确定即可。如图1-3所示。现在再编译项目即可编译通过。到此Openssl的环境配置就完成了，可以尝试调用一下其中的函数，后面会通过一个案例来使用Openssl库。\n图1-2\n图1-3\n在UE4C++项目中使用 在 UE4项目中配置Openssl环境（UE4中引用第三方链接库）。 在UE4项目中无法按照文章“在Windows下配置Openssl环境”配置相关环境，UE4的项目属性中没有“链接器-\u0026gt;输入”，所以无法指定第三方静态链接库。UE4引用第三方静态链接库需要在“.Build.cs”文件中配置。 首先在插件的根目录的下创建文件夹“ThirdParty”，然后去“Openssl”的安装目录下找到文件夹“OpenSSL-Win64”（我这里的目录为“C:\\Program Files\\OpenSSL-Win64”），最后将这个文件夹复制到“ThirdParty”中，并保留文件夹“Include”、“Lib”两个文件夹，其余全删除。如图1-4所示。\n图1-4\n在VS工程模块中的“.build.cs”文件中加载第三方静态链接库，代码如下所示。 // Some copyright should be here\u0026hellip; using System.IO; using UnrealBuildTool;\npublic class SimpleHttp : ModuleRules { /\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;Begin\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;/ private string ModulePath { // get { return Path.GetDirectoryName(RulesCompiler.GetModuleFilename(this.GetType().Name)); } get { return ModuleDirectory; } }\nprivate string ThirdPartyPath { get { return Path.","title":"在Windows下配置Openssl环境"},{"content":"一、问题描述 在开发类似飞行类游戏的时候往往需要将相机的旋转范围设置到[-180~180]，但引擎默认会将pitch和roll两个轴向限定到[-90~90]，这并不是一个bug，是引擎为了适配FPS游戏所做的限制，你可以垂直向上或者向下看，超过这个角度就没有意义了且会导致万向节锁的现象，为了避免这种现象我们可以采用四元数的方法。\n二、解决步骤 使用的是飞行模板，创建自己的GameMode和Controller和pawn并在worldSettings中设置为我们的GameMode。如图示1.1（注意Pawn类中要加入Camera组件）\n1 2 图示1.1 项目设置中添加输入，如图示1.2\n1 2 图示1.2 接下来是主要的一步，使用四元数的方法解决万向节锁的现象。为了方便使用，我这里写成了一个插件。如图1.3所示，选择模板“BlueprintLibrary”，创建一个插件。\n1 2 图示1.3 打开VS，将如下代码加入到.h和.cpp中。注意：在cpp中将函数名称“UMyFunctionName”替换为自己的类名称。如图1.4所示\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 public: // 旋转公式:将欧拉角的度数转换为四元数 UFUNCTION(BlueprintCallable, meta = (DisplayName = \u0026#34;Euler To Quaternion\u0026#34;, Keywords = \u0026#34;rotation, quaterion\u0026#34;), Category = \u0026#34;Quaternion Rotation\u0026#34;) static FQuat Euler_To_Quaternion(FRotator Current_Rotation); // 根据输入的四元数设置组件的世界坐标的旋转 UFUNCTION(BlueprintCallable, meta = (DisplayName = \u0026#34;Set World Rotation (Quaterion)\u0026#34;, Keywords = \u0026#34;rotation, quaternion\u0026#34;), Category = \u0026#34;Quaternion Rotation\u0026#34;) static void SetWorldRotationQuat(USceneComponent* SceneComponent, const FQuat\u0026amp; Desired_Rotation); // 根据输入的四元数设置组件的相对旋转 UFUNCTION(BlueprintCallable, meta = (DisplayName = \u0026#34;Set Relative Rotation (Quaterion)\u0026#34;, Keywords = \u0026#34;rotation, quaternion\u0026#34;), Category = \u0026#34;Quaternion Rotation\u0026#34;) static void SetRelativeRotationQuat(USceneComponent* SceneComponent, const FQuat\u0026amp; Desired_Rotation); // 添加本地旋转量 UFUNCTION(BlueprintCallable, meta = (DisplayName = \u0026#34;Add Local Rotation (Quaterion)\u0026#34;, Keywords = \u0026#34;rotation, quaternion\u0026#34;), Category = \u0026#34;Quaternion Rotation\u0026#34;) static void AddLocalRotationQuat(USceneComponent* SceneComponent, const FQuat\u0026amp; Delta_Rotation); // 根据输入的四元数设置Actor的世界坐标的旋转 UFUNCTION(BlueprintCallable, meta = (DisplayName = \u0026#34;Set Actor World Rotation (Quaternion)\u0026#34;, Keywords = \u0026#34;rotation, quaternion\u0026#34;), Category = \u0026#34;Quaternion Rotation\u0026#34;) static void SetActorWorldRotationQuat(AActor* Actor, const FQuat\u0026amp; Desired_Rotation); //根据输入的四元数设置Actor的相对旋转 UFUNCTION(BlueprintCallable, meta = (DisplayName = \u0026#34;Set Actor Relative Rotation (Quaternion)\u0026#34;, Keywords = \u0026#34;rotation, quaternion\u0026#34;), Category = \u0026#34;Quaternion Rotation\u0026#34;) static void SetActorRelativeRotationQuat(AActor* Actor, const FQuat\u0026amp; Desired_Rotation); // 添加本地旋转量 UFUNCTION(BlueprintCallable, meta = (DisplayName = \u0026#34;Add Actor Local Rotation (Quaternion)\u0026#34;, Keywords = \u0026#34;rotation, quaternion\u0026#34;), Category = \u0026#34;Quaternion Rotation\u0026#34;) static void AddActorLocalRotationQuat(AActor* Actor, const FQuat\u0026amp; Delta_Rotation); FQuat UQuaternoinsBPLibrary::Euler_To_Quaternion(FRotator Current_Rotation) { //声明输出四元数 FQuat q; //将度数转换为弧度 float yaw = Current_Rotation.Yaw * PI / 180; float roll = Current_Rotation.Roll * PI / 180; float pitch = Current_Rotation.Pitch * PI / 180; double cy = cos(yaw * 0.5); double sy = sin(yaw * 0.5); double cr = cos(roll * 0.5); double sr = sin(roll * 0.5); double cp = cos(pitch * 0.5); double sp = sin(pitch * 0.5); q.W = cy * cr * cp + sy * sr * sp; q.X = cy * sr * cp - sy * cr * sp; q.Y = cy * cr * sp + sy * sr * cp; q.Z = sy * cr * cp - cy * sr * sp; return q; } void UQuaternoinsBPLibrary::SetWorldRotationQuat(USceneComponent* SceneComponent, const FQuat\u0026amp; Desired_Rotation) { if (SceneComponent) { SceneComponent-\u0026gt;SetWorldRotation(Desired_Rotation); } } void UQuaternoinsBPLibrary::SetRelativeRotationQuat(USceneComponent* SceneComponent, const FQuat\u0026amp; Desired_Rotation) { if (SceneComponent) { SceneComponent-\u0026gt;SetRelativeRotation(Desired_Rotation); } } void UQuaternoinsBPLibrary::AddLocalRotationQuat(USceneComponent* SceneComponent, const FQuat\u0026amp; Delta_Rotation) { if (SceneComponent) { SceneComponent-\u0026gt;AddLocalRotation(Delta_Rotation); } } void UQuaternoinsBPLibrary::SetActorWorldRotationQuat(AActor* Actor, const FQuat\u0026amp; Desired_Rotation) { if (Actor) { Actor-\u0026gt;SetActorRotation(Desired_Rotation); } } void UQuaternoinsBPLibrary::SetActorRelativeRotationQuat(AActor* Actor, const FQuat\u0026amp; Desired_Rotation) { if (Actor) { Actor-\u0026gt;SetActorRelativeRotation(Desired_Rotation); } } void UQuaternoinsBPLibrary::AddActorLocalRotationQuat(AActor* Actor, const FQuat\u0026amp; Delta_Rotation) { if (Actor) { Actor-\u0026gt;AddActorLocalRotation(Delta_Rotation); } } 1 2 图示1.4 编译并打开工程，在Controller里面写入如下逻辑，如图1.5所示。这样就可以解决万向节锁的问题。\n1 2 图示1.5 参考链接 https://www.youtube.com/watch?v=KqbqZ3IY1II\u0026amp;list=PLyu-W38DvZhqOdaCkB4hGWYQzuKH2gALZ\u0026amp;index=2\n","permalink":"https://imrcao.top/posts/%E5%BD%92%E6%A1%A3/%E8%A7%A3%E5%86%B3ue4%E4%B8%87%E5%90%91%E8%8A%82%E9%94%81gimbal-lock%E7%9A%84%E9%97%AE%E9%A2%98/","summary":"一、问题描述 在开发类似飞行类游戏的时候往往需要将相机的旋转范围设置到[-180~180]，但引擎默认会将pitch和roll两个轴向限定到[-90~90]，这并不是一个bug，是引擎为了适配FPS游戏所做的限制，你可以垂直向上或者向下看，超过这个角度就没有意义了且会导致万向节锁的现象，为了避免这种现象我们可以采用四元数的方法。\n二、解决步骤 使用的是飞行模板，创建自己的GameMode和Controller和pawn并在worldSettings中设置为我们的GameMode。如图示1.1（注意Pawn类中要加入Camera组件）\n1 2 图示1.1 项目设置中添加输入，如图示1.2\n1 2 图示1.2 接下来是主要的一步，使用四元数的方法解决万向节锁的现象。为了方便使用，我这里写成了一个插件。如图1.3所示，选择模板“BlueprintLibrary”，创建一个插件。\n1 2 图示1.3 打开VS，将如下代码加入到.h和.cpp中。注意：在cpp中将函数名称“UMyFunctionName”替换为自己的类名称。如图1.4所示\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 public: // 旋转公式:将欧拉角的度数转换为四元数 UFUNCTION(BlueprintCallable, meta = (DisplayName = \u0026#34;Euler To Quaternion\u0026#34;, Keywords = \u0026#34;rotation, quaterion\u0026#34;), Category = \u0026#34;Quaternion Rotation\u0026#34;) static FQuat Euler_To_Quaternion(FRotator Current_Rotation); // 根据输入的四元数设置组件的世界坐标的旋转 UFUNCTION(BlueprintCallable, meta = (DisplayName = \u0026#34;Set World Rotation (Quaterion)\u0026#34;, Keywords = \u0026#34;rotation, quaternion\u0026#34;), Category = \u0026#34;Quaternion Rotation\u0026#34;) static void SetWorldRotationQuat(USceneComponent* SceneComponent, const FQuat\u0026amp; Desired_Rotation); // 根据输入的四元数设置组件的相对旋转 UFUNCTION(BlueprintCallable, meta = (DisplayName = \u0026#34;Set Relative Rotation (Quaterion)\u0026#34;, Keywords = \u0026#34;rotation, quaternion\u0026#34;), Category = \u0026#34;Quaternion Rotation\u0026#34;) static void SetRelativeRotationQuat(USceneComponent* SceneComponent, const FQuat\u0026amp; Desired_Rotation); // 添加本地旋转量 UFUNCTION(BlueprintCallable, meta = (DisplayName = \u0026#34;Add Local Rotation (Quaterion)\u0026#34;, Keywords = \u0026#34;rotation, quaternion\u0026#34;), Category = \u0026#34;Quaternion Rotation\u0026#34;) static void AddLocalRotationQuat(USceneComponent* SceneComponent, const FQuat\u0026amp; Delta_Rotation); // 根据输入的四元数设置Actor的世界坐标的旋转 UFUNCTION(BlueprintCallable, meta = (DisplayName = \u0026#34;Set Actor World Rotation (Quaternion)\u0026#34;, Keywords = \u0026#34;rotation, quaternion\u0026#34;), Category = \u0026#34;Quaternion Rotation\u0026#34;) static void SetActorWorldRotationQuat(AActor* Actor, const FQuat\u0026amp; Desired_Rotation); //根据输入的四元数设置Actor的相对旋转 UFUNCTION(BlueprintCallable, meta = (DisplayName = \u0026#34;Set Actor Relative Rotation (Quaternion)\u0026#34;, Keywords = \u0026#34;rotation, quaternion\u0026#34;), Category = \u0026#34;Quaternion Rotation\u0026#34;) static void SetActorRelativeRotationQuat(AActor* Actor, const FQuat\u0026amp; Desired_Rotation); // 添加本地旋转量 UFUNCTION(BlueprintCallable, meta = (DisplayName = \u0026#34;Add Actor Local Rotation (Quaternion)\u0026#34;, Keywords = \u0026#34;rotation, quaternion\u0026#34;), Category = \u0026#34;Quaternion Rotation\u0026#34;) static void AddActorLocalRotationQuat(AActor* Actor, const FQuat\u0026amp; Delta_Rotation); FQuat UQuaternoinsBPLibrary::Euler_To_Quaternion(FRotator Current_Rotation) { //声明输出四元数 FQuat q; //将度数转换为弧度 float yaw = Current_Rotation.","title":"解决UE4万向节锁（Gimbal Lock）的问题"},{"content":"引言 在日常的开发中会遇到很多使用C++来加载资源的情况，例如我们要引用Content下的蓝图类和非蓝图类，那么在UE4中提供了很多机制来引用这些资源。从网上查到的很多资料来看，很多游戏开发者将这种机制称之为“静态加载”和“动态加载”，但是从官方文档上来看，我并没有搜到静态加载相关的词语，官方将这种机制称之为“硬性引用(HardRefernences)”和“软性应用(SoftReferences)”。在这篇文章中我会把网上的资料和官方资料整理一下，也方便以后查阅。\n静态加载-硬性引用 我这里理解的是静态加载和硬性引用一个意思，对应的动态加载和软性引用一个意思。硬性应用由两种场景的情况。 （1）直接属性引用：这是最常见的资源引用情况，通过宏UPROPERTY暴露给蓝图，这样可以允许设计人员在编辑器中指定相关资源。示例代码如下： /** construction start sound stinger */\nUPROPERTY(EditDefaultsOnly, Category=Building)\nUSoundCue* ConstructionStartStinger;\n（2）构造时引用：在构造函数引用确切的资源，这里是使用特殊的类ConstructorHelpers完成的，这个类中有两个对应的方法：FObjectFinder()和FClassFinder()，它们分别加载的是非蓝图资产（动画、贴图、模型、音效等）和蓝图类（角色蓝图、Widget控件）资产。示例代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 static ConstructorHelpers::FClassFinder\u0026lt;AChatOnline_WFYCharacter\u0026gt; PlayerPawnBPClass(TEXT(\u0026#34;/Game/ChatOnline/WFY_Blueprint/Core/WFY_Character\u0026#34;)); static ConstructorHelpers::FClassFinder\u0026lt;AHUD\u0026gt; PlayerHUDBPClass(TEXT(\u0026#34;/Game/ChatOnline/WFY_Blueprint/Core/ChatOnline_WFYHUD_BP\u0026#34;)); //DefaultPawnClass = AChatOnline_WFYCharacter::StaticClass(); if (PlayerPawnBPClass.Class!=NULL) { DefaultPawnClass = PlayerPawnBPClass.Class; } if (PlayerHUDBPClass.Class!=NULL) { HUDClass = PlayerHUDBPClass.Class; } class UTexture2D* BarFillTexture; static ConstructorHelpers::FObjectFinder\u0026lt;UTexture2D\u0026gt; BarFillObj(TEXT(\u0026#34;/Game/UI/HUD/BarFill\u0026#34;)); BarFillTexture = BarFillObj.Object; （了解更多：在以上构造函数中，ConstructorHelpers 类将尝试在内存中查找该资源，如果找不到，则进行加载。请注意，使用资源的完整路径来指定要加载的内容。如果该资源不存在或者由于出错而无法加载，那么该属性将设置为 nullptr。发生这种情况时，尝试访问纹理的代码将崩溃。最好进行声明，指出资源已正确加载（如果后续代码假设引用有效）。 UPROPERTY 的声明与前面的硬性引用示例相同。它们的工作方式相同，只不过是最初的设置方式有所差别。有关硬性引用的一个注意事项是，当对象加载并实例化时，还将加载以硬性方式引用的资源。您必须仔细地进行考虑，否则内存使用量会因为同时加载许多资源而迅速增加。如果您希望推迟该加载或确定要在运行时加载的内容，那么下列各节可以帮助您完成这些任务。）\n动态加载-软性引用 动态加载最常用的两个模板化方法是LoadObject\u0026lt;\u0026gt;()和LoadClass\u0026lt;\u0026gt;()。同样的前者加载非蓝图类资产后者加载蓝图类资产。需要注意的是使用LoadClass时，路径名必须带_C后缀，LoadObject不需要带后缀。示例代码如下： //加载Content目录下的资源文件 UTexture2D* SpeackCheck; UTexture2D* MicCheck; //方法一： SpeackCheck = LoadObject(nullptr, TEXT(\u0026quot;/Game/ChatOnline/WFY_Textture/Icon/Speaker_Check\u0026quot;));\n//方法二 FSoftObjectPath softObjectPath(TEXT(\u0026quot;/Game/ChatOnline/WFY_Textture/Icon/Mic_check\u0026quot;));\n1 2 3 4 5 6 7 8 UObject* object = softObjectPath.TryLoad(); MicCheck = Cast\u0026lt;UTexture2D\u0026gt;(object) //LoadClass\u0026lt;\u0026gt;() TSubclassOf\u0026lt;UUI_TopTitle\u0026gt; TopTitle = LoadClass\u0026lt;UUI_TopTitle\u0026gt;(nullptr, TEXT(\u0026#34;/Game/ChatOnline/WFY_Widget/Common/CharacterTopTitle.CharacterTopTitle_C\u0026#34;)); pTopTitle = CreateWidget\u0026lt;UUI_TopTitle\u0026gt;(GetWorld(), TopTitlef.Class); Node： 1 ConstructorHelpers::FClassFinder\u0026lt;\u0026gt;()和ConstructorHelpers::FObjectFinder()不能在构造函数之外使用，否则会报错如下“FObjectFinders can\u0026#39;t be used outside of constructors to find %s”，其他地方可以使用LoadClass()和LoadObject()。 在实际开发过程中遇到的一些需要注意的点，在继承自“UUserWidget”的类中，不能在函数“NativeConstruct()”中调用ConstructorHelpers::FObjectFinder()和FClassFinder()，否则也会报同样的错误。正确示例代码：\nUCLASS()\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class UUI_TopTitle : public UUserWidget { GENERATED_BODY() public: virtual void NativeConstruct(); virtual void NativeDestruct(); virtual void NativeTick(const FGeometry\u0026amp; MyGeometry, float InDeltaTime); ｝ void UUI_TopTitle::NativeConstruct() { Super::NativeConstruct(); Speaking = LoadObject\u0026lt;UTexture2D\u0026gt;(nullptr, TEXT(\u0026#34;/Game/ChatOnline/WFY_Textture/Icon/Speaker_Check\u0026#34;)); NoSpeaker = LoadObject\u0026lt;UTexture2D\u0026gt;(nullptr, TEXT(\u0026#34;/Game/ChatOnline/WFY_Textture/Icon/Speaker_Uncheck\u0026#34;)); ｝ 参考链接： https://docs.unrealengine.com/4.27/zh-CN/ProgrammingAndScripting/ProgrammingWithCPP/Assets/ReferencingAssets/\nhttps://blog.csdn.net/yb0022/article/details/103104215\n知乎 https://zhuanlan.zhihu.com/p/266859719\nhttps://www.pianshen.com/article/3032128253/\n","permalink":"https://imrcao.top/posts/%E5%BD%92%E6%A1%A3/ue4%E7%9A%84%E9%9D%99%E6%80%81%E5%8A%A0%E8%BD%BD%E5%92%8C%E5%8A%A8%E6%80%81%E5%8A%A0%E8%BD%BD%E5%BC%95%E7%94%A8%E8%B5%84%E6%BA%90/","summary":"引言 在日常的开发中会遇到很多使用C++来加载资源的情况，例如我们要引用Content下的蓝图类和非蓝图类，那么在UE4中提供了很多机制来引用这些资源。从网上查到的很多资料来看，很多游戏开发者将这种机制称之为“静态加载”和“动态加载”，但是从官方文档上来看，我并没有搜到静态加载相关的词语，官方将这种机制称之为“硬性引用(HardRefernences)”和“软性应用(SoftReferences)”。在这篇文章中我会把网上的资料和官方资料整理一下，也方便以后查阅。\n静态加载-硬性引用 我这里理解的是静态加载和硬性引用一个意思，对应的动态加载和软性引用一个意思。硬性应用由两种场景的情况。 （1）直接属性引用：这是最常见的资源引用情况，通过宏UPROPERTY暴露给蓝图，这样可以允许设计人员在编辑器中指定相关资源。示例代码如下： /** construction start sound stinger */\nUPROPERTY(EditDefaultsOnly, Category=Building)\nUSoundCue* ConstructionStartStinger;\n（2）构造时引用：在构造函数引用确切的资源，这里是使用特殊的类ConstructorHelpers完成的，这个类中有两个对应的方法：FObjectFinder()和FClassFinder()，它们分别加载的是非蓝图资产（动画、贴图、模型、音效等）和蓝图类（角色蓝图、Widget控件）资产。示例代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 static ConstructorHelpers::FClassFinder\u0026lt;AChatOnline_WFYCharacter\u0026gt; PlayerPawnBPClass(TEXT(\u0026#34;/Game/ChatOnline/WFY_Blueprint/Core/WFY_Character\u0026#34;)); static ConstructorHelpers::FClassFinder\u0026lt;AHUD\u0026gt; PlayerHUDBPClass(TEXT(\u0026#34;/Game/ChatOnline/WFY_Blueprint/Core/ChatOnline_WFYHUD_BP\u0026#34;)); //DefaultPawnClass = AChatOnline_WFYCharacter::StaticClass(); if (PlayerPawnBPClass.Class!=NULL) { DefaultPawnClass = PlayerPawnBPClass.Class; } if (PlayerHUDBPClass.Class!=NULL) { HUDClass = PlayerHUDBPClass.Class; } class UTexture2D* BarFillTexture; static ConstructorHelpers::FObjectFinder\u0026lt;UTexture2D\u0026gt; BarFillObj(TEXT(\u0026#34;/Game/UI/HUD/BarFill\u0026#34;)); BarFillTexture = BarFillObj.Object; （了解更多：在以上构造函数中，ConstructorHelpers 类将尝试在内存中查找该资源，如果找不到，则进行加载。请注意，使用资源的完整路径来指定要加载的内容。如果该资源不存在或者由于出错而无法加载，那么该属性将设置为 nullptr。发生这种情况时，尝试访问纹理的代码将崩溃。最好进行声明，指出资源已正确加载（如果后续代码假设引用有效）。 UPROPERTY 的声明与前面的硬性引用示例相同。它们的工作方式相同，只不过是最初的设置方式有所差别。有关硬性引用的一个注意事项是，当对象加载并实例化时，还将加载以硬性方式引用的资源。您必须仔细地进行考虑，否则内存使用量会因为同时加载许多资源而迅速增加。如果您希望推迟该加载或确定要在运行时加载的内容，那么下列各节可以帮助您完成这些任务。）\n动态加载-软性引用 动态加载最常用的两个模板化方法是LoadObject\u0026lt;\u0026gt;()和LoadClass\u0026lt;\u0026gt;()。同样的前者加载非蓝图类资产后者加载蓝图类资产。需要注意的是使用LoadClass时，路径名必须带_C后缀，LoadObject不需要带后缀。示例代码如下： //加载Content目录下的资源文件 UTexture2D* SpeackCheck; UTexture2D* MicCheck; //方法一： SpeackCheck = LoadObject(nullptr, TEXT(\u0026quot;/Game/ChatOnline/WFY_Textture/Icon/Speaker_Check\u0026quot;));","title":"UE4的静态加载和动态加载（引用资源）"},{"content":"前言： 最近在写使用UE4联动“六轴平台”的控制程序，这里就用到了机器硬件交互和计算机网络相关的知识。拿到硬件厂商的网络协议后，就要根据协议编写相关代码，下面记录一下开发过程。\n网络字节序： 首先要明白和机器通讯中所传输的数据一般为字节流，即网络字节序，顾名思义就是字节的顺序。一般系统都会采用 UDP协议，UDP的好处就是机器只管接收就行。\n进行 UDP广播： 在开始前，你需要了解如何使用UE4进行 UDP广播数据，你可以参考这篇文章“使用UE4原生模块进行简单UDP广播和数据接收”。\nfloat转字节序： 直接上代码：这个函数可以将float类型的数据转为unsigned char类型的数组。 /*\nDescribe:将float类型数据转换为unsigned char数组（即网络字节序） Paramfvalue待转换数据 Paramarr传入的字节序 ParamIndex从数组的第几位开始转换，默认为0 **/ 1 2 3 4 void AUDPSend::ftoc(float fvalue, int Index, unsigned char* arr) { unsigned char* pf; unsigned char* px; unsigned char i; //计数器 pf = (unsigned char*)\u0026amp;fvalue; /unsigned char型指针取得浮点数的首地址/ px = arr; /字符数组arr准备存储浮点数的四个字节,px指针指向字节数组arr/ for (i = Index; i \u0026lt; Index + 4; i++) { *(px + i) = *(pf + (i - Index)); /使用unsigned char型指针从低地址一个字节一个字节取出/ }\n示例： 1、假设一个六维平台采用的是 UDP协议，且接收的数据结构如下： USTRUCT(BlueprintType)\n1 2 3 4 5 6 7 8 9 10 11 12 13 struct FDataToVice { GENERATED_USTRUCT_BODY() UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = \u0026#34;DataToVice\u0026#34;) int sMark = 55; UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = \u0026#34;DataToVice\u0026#34;) int sComd = 0; int sRepa[2] = { 0 }; float sAtti[6]; float sVelo[6] = { 0 }; float sAcce[3] = { 0 }; }; 2、进行 UDP广播之前，应该先构造需要广播的数据。意思就是将结构体中的数据转为字节流。\n创建一个“unsigned char”类型数组： unsigned char ViceData[64] = {0};\n用于构造数据的函数如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 void AUDPSend::MakeViceData(FTransform Transform_, FDataToVice DataToVice_) { DataToVice_.sAtti[0] = Transform_.Rotator().Pitch; DataToVice_.sAtti[1] = Transform_.Rotator().Yaw; DataToVice_.sAtti[2] = Transform_.Rotator().Roll; DataToVice_.sAtti[3] = Transform_.GetTranslation().X; DataToVice_.sAtti[4] = Transform_.GetTranslation().Y; DataToVice_.sAtti[5] = Transform_.GetTranslation().Z; ViceData[0] = DataToVice_.sMark; ViceData[1] = DataToVice_.sComd; ViceData[2] = DataToVice_.sRepa[0]; ViceData[3] = DataToVice_.sRepa[1]; //float Vector_X = Transform_.GetTranslation().X; //ftoc(Vector_X, 4, ViceData); ftocLoop(DataToVice_.sAtti, 4, 6, ViceData); ftocLoop(DataToVice_.sVelo, 28, 6, ViceData); ftocLoop(DataToVice_.sAcce, 52, 3, ViceData); } void AUDPSend::ftocLoop(float Value[],int BeginIndex, int length, unsigned char* arr) { for (int i = 0; i \u0026lt; length; i++) { ftoc(Value[i], BeginIndex, ViceData); BeginIndex += 4; } } 开始UDP广播数据：（建议先阅读文章“使用UE4原生模块进行简单UDP广播和数据接收”） 在蓝图中先调用“构造数据的函数MakeViceData”，再调用函数“SendUDPMessages”就可以开始广播数据了\n1 2 3 4 5 6 7 8 9 bool AUDPSend::SendUDPMessages(FString ToSend) { if (!SenderSocket) { //ScreenMsg(\u0026#34;No sender socket\u0026#34;); return false; } int32 BytesSent = 0; SenderSocket-\u0026gt;SendTo(ViceData, 64, BytesSent, *RemoteAddr);//发送给远端地址\nif (BytesSent \u0026lt;= 0) {\n1 2 3 4 5 6 7 8 const FString Str = \u0026#34;Socket is valid but the receiver received 0 bytes, make sure it is listening properly!\u0026#34;; UE_LOG(LogTemp, Error, TEXT(\u0026#34;%s\u0026#34;), *Str); ScreenMsg(Str); return false; } return true; } 使用网络抓包工具查看网络中的字节数据： 可以使用网络抓包工具对网络中的数据进行查看，网上这种工具很多，我这里使用的是“NetAssist”，（安装包放在了文章结尾处的百度云链接中，见资源01）。 下载好直接运行即可，UDP开始广播后，输入对应的 IP和端口，开始监听就可以看到数据了。如图1-1所示： 图1-1\n参考链接： https://blog.csdn.net/u013457167/article/details/78323298\n资源链接： 链接：https://pan.baidu.com/s/1RB_fYao2CCyNeKao5L3p8Q 提取码：Mcao\n","permalink":"https://imrcao.top/posts/%E5%BD%92%E6%A1%A3/%E6%B5%AE%E7%82%B9%E5%9E%8B%E6%95%B0%E6%8D%AE%E8%BD%AC%E6%8D%A2%E4%B8%BA%E7%BD%91%E7%BB%9C%E5%AD%97%E8%8A%82%E5%BA%8F/","summary":"前言： 最近在写使用UE4联动“六轴平台”的控制程序，这里就用到了机器硬件交互和计算机网络相关的知识。拿到硬件厂商的网络协议后，就要根据协议编写相关代码，下面记录一下开发过程。\n网络字节序： 首先要明白和机器通讯中所传输的数据一般为字节流，即网络字节序，顾名思义就是字节的顺序。一般系统都会采用 UDP协议，UDP的好处就是机器只管接收就行。\n进行 UDP广播： 在开始前，你需要了解如何使用UE4进行 UDP广播数据，你可以参考这篇文章“使用UE4原生模块进行简单UDP广播和数据接收”。\nfloat转字节序： 直接上代码：这个函数可以将float类型的数据转为unsigned char类型的数组。 /*\nDescribe:将float类型数据转换为unsigned char数组（即网络字节序） Paramfvalue待转换数据 Paramarr传入的字节序 ParamIndex从数组的第几位开始转换，默认为0 **/ 1 2 3 4 void AUDPSend::ftoc(float fvalue, int Index, unsigned char* arr) { unsigned char* pf; unsigned char* px; unsigned char i; //计数器 pf = (unsigned char*)\u0026amp;fvalue; /unsigned char型指针取得浮点数的首地址/ px = arr; /字符数组arr准备存储浮点数的四个字节,px指针指向字节数组arr/ for (i = Index; i \u0026lt; Index + 4; i++) { *(px + i) = *(pf + (i - Index)); /使用unsigned char型指针从低地址一个字节一个字节取出/ }","title":"浮点型数据转换为网络字节序"},{"content":"介绍： 罗技的赛钛克手柄（logitech-saitak X52）主要用于飞行类游戏，使用这个飞行控制器模拟飞行具有不错的体验。虚幻引擎作为一个游戏引擎应该也支持这类手柄，但虚幻引擎并不能做直接接收赛钛克手柄输入（不像XBox360，它可以直接输入进引擎）。下面记录一下如何使用Saitek手柄接入到虚幻引擎。\n环境： 虚幻引擎425、Saitek X52\n步骤一 首先安装Saitek X52控制器驱动，可以去官网下载（官网见：参考资料）。下载好的驱动直接点击安装即可。 安装好驱动后打开“设备管理器”，找到“人体学输入设备”，在下拉列表中找到对应的控制设备，打开“属性-\u0026gt;详细信息-\u0026gt;-硬件ID”。“VID_”后面的“06A3”和“PID_”后面的“075C”分别为品牌ID和设备ID。如图1-1所示。这两个值为16进制数，记下这两个值，后面引擎中会用到。\n图1-1\n步骤二 使用虚幻引擎蓝图版本的飞机模板（Flying），在Plugins中找到并勾选插件“Windows Rawinput”，重启后在项目设置中找到“Plugins-\u0026gt;Raw Input”。插件“Windows RawInput”提供了八个轴向的输入和二十个按钮输入。 添加一个设备配置插槽，Vendor ID和Product ID分别填入“0x06A3”和“0x075C”。在属性添加一个轴向属性，勾选上“Enable”和“Gamepad stick”。勾选上“Gamepad stick”的意思是让输入变为“-1到1”而不是“0-1”。如图2-1所示。 打开“Engine-\u0026gt;Input”，在轴向映射下面的“MoveUp”一栏添加刚刚的一个轴向输入“GenericUSBController Axis 1”。如图2-2所示。这样手柄就可以进行输入了。\n图2-1\n图2-2\n三、下面记录一下八个轴向的输入和二十个按钮输入对应关系，如图3-1所示。 图3-1\n四、Saitek X52支持的一些游戏，如图4-1所示。 图4-1\n参考资料： saitek官网： https://www.logitechg.com/en-gb/products/space/x56-space-flight-vr-simulator-controller.945-000059.html\n论坛： https://forums.unrealengine.com/t/3-new-plugins-joystick-hotas-support-blueprint-easing-functions-saitek-x52-pro/6620\n官方文档 https://docs.unrealengine.com/4.26/zh-CN/InteractiveExperiences/Input/RawInput/\n","permalink":"https://imrcao.top/posts/%E5%BD%92%E6%A1%A3/%E4%BD%BF%E7%94%A8saitekx52%E6%8E%A5%E5%85%A5%E5%88%B0ue4/","summary":"介绍： 罗技的赛钛克手柄（logitech-saitak X52）主要用于飞行类游戏，使用这个飞行控制器模拟飞行具有不错的体验。虚幻引擎作为一个游戏引擎应该也支持这类手柄，但虚幻引擎并不能做直接接收赛钛克手柄输入（不像XBox360，它可以直接输入进引擎）。下面记录一下如何使用Saitek手柄接入到虚幻引擎。\n环境： 虚幻引擎425、Saitek X52\n步骤一 首先安装Saitek X52控制器驱动，可以去官网下载（官网见：参考资料）。下载好的驱动直接点击安装即可。 安装好驱动后打开“设备管理器”，找到“人体学输入设备”，在下拉列表中找到对应的控制设备，打开“属性-\u0026gt;详细信息-\u0026gt;-硬件ID”。“VID_”后面的“06A3”和“PID_”后面的“075C”分别为品牌ID和设备ID。如图1-1所示。这两个值为16进制数，记下这两个值，后面引擎中会用到。\n图1-1\n步骤二 使用虚幻引擎蓝图版本的飞机模板（Flying），在Plugins中找到并勾选插件“Windows Rawinput”，重启后在项目设置中找到“Plugins-\u0026gt;Raw Input”。插件“Windows RawInput”提供了八个轴向的输入和二十个按钮输入。 添加一个设备配置插槽，Vendor ID和Product ID分别填入“0x06A3”和“0x075C”。在属性添加一个轴向属性，勾选上“Enable”和“Gamepad stick”。勾选上“Gamepad stick”的意思是让输入变为“-1到1”而不是“0-1”。如图2-1所示。 打开“Engine-\u0026gt;Input”，在轴向映射下面的“MoveUp”一栏添加刚刚的一个轴向输入“GenericUSBController Axis 1”。如图2-2所示。这样手柄就可以进行输入了。\n图2-1\n图2-2\n三、下面记录一下八个轴向的输入和二十个按钮输入对应关系，如图3-1所示。 图3-1\n四、Saitek X52支持的一些游戏，如图4-1所示。 图4-1\n参考资料： saitek官网： https://www.logitechg.com/en-gb/products/space/x56-space-flight-vr-simulator-controller.945-000059.html\n论坛： https://forums.unrealengine.com/t/3-new-plugins-joystick-hotas-support-blueprint-easing-functions-saitek-x52-pro/6620\n官方文档 https://docs.unrealengine.com/4.26/zh-CN/InteractiveExperiences/Input/RawInput/","title":"使用SaitekX52接入到UE4"},{"content":"VirtualCamera是虚幻引擎中的工程示例项目。虚幻引擎4.26版本中将其升级到了2.0（Beta版本），功能和UI都全面更新（如图1-1所示）。虚拟摄像机在虚幻引擎中驱动电影摄像机（Cine Camera），它能够将结果输出到各种外部设备中。我使用它主要用来拍摄镜头预演。 演示连接：https://www.bilibili.com/video/BV195411w75h/\n图1-1\nVirtualCamera如何在输出上覆盖自定义的UMG控件\n我这边采用的是“BlackMagic”采集卡（型号：）通过SDI线将画面传输到监视设备。根据引擎自带的“BlackMagic”插件是能够将画面采集并输出到监视设备的，但并不能将“UMG”控件采集并输出。“VirtualCamera”中可以实现这样的功能。下面简单分析一下这一块的逻辑是如何执行的。 UMG是在Vcam组件属性中OutputPrivider中指定的，打开源码找到对应的文件 VcamOutputProviderBase.h。简单阅读一遍后很容易就能发现逻辑是从初始化函数“Initialize()”开始执行的，经过一系列判断后一次执行“Acitvate()”、“CreateUMG()”和“DisplayUMG()”。具体代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 void UVCamOutputProviderBase::CreateUMG() { if (!UMGClass) { return; } if (UMGWidget) { UE_LOG(LogVCamOutputProvider, Error, TEXT(\u0026#34;CreateUMG widget already set - failed to create\u0026#34;)); return; } UMGWidget = NewObject\u0026lt;UVPFullScreenUserWidget\u0026gt;(this, UVPFullScreenUserWidget::StaticClass()); UMGWidget-\u0026gt;SetDisplayTypes(DisplayType, DisplayType, DisplayType); UMGWidget-\u0026gt;PostProcessDisplayType.bReceiveHardwareInput = true; #if WITH_EDITOR UMGWidget-\u0026gt;SetAllTargetViewports(GetTargetLevelViewport()); #endif UMGWidget-\u0026gt;WidgetClass = UMGClass; UE_LOG(LogVCamOutputProvider, Log, TEXT(\u0026#34;CreateUMG widget named %s from class %s\u0026#34;), *UMGWidget-\u0026gt;GetName(), *UMGWidget-\u0026gt;WidgetClass-\u0026gt;GetName()); } void UVCamOutputProviderBase::DisplayUMG() { if (UMGWidget) { UWorld* ActorWorld = nullptr; int32 WorldType = -1; for (const FWorldContext\u0026amp; Context : GEngine-\u0026gt;GetWorldContexts()) { if (Context.World()) { // Prioritize PIE and Game modes if active if ((Context.WorldType == EWorldType::PIE) || (Context.WorldType == EWorldType::Game)) { ActorWorld = Context.World(); WorldType = (int32)Context.WorldType; break; } else if (Context.WorldType == EWorldType::Editor) { // Only grab the Editor world if PIE and Game aren\u0026#39;t available ActorWorld = Context.World(); WorldType = (int32)Context.WorldType; } } } if (ActorWorld) { UMGWidget-\u0026gt;Display(ActorWorld); UE_LOG(LogVCamOutputProvider, Log, TEXT(\u0026#34;DisplayUMG widget displayed in WorldType %d\u0026#34;), WorldType); } NotifyWidgetOfComponentChange(); } } 进入“UMGWidget-\u0026gt;Display(ActorWorld);”函数中。会发现是使用Slate，SNew出的控件。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 bool FVPFullScreenUserWidget_Viewport::Display(UWorld* World, UUserWidget* Widget, float InDPIScale) { TSharedPtr\u0026lt;SConstraintCanvas\u0026gt; FullScreenWidgetPinned = FullScreenCanvasWidget.Pin(); if (Widget == nullptr || World == nullptr || FullScreenWidgetPinned.IsValid()) { return false; } UGameViewportClient* ViewportClient = nullptr; #if WITH_EDITOR TSharedPtr\u0026lt;SLevelViewport\u0026gt; ActiveLevelViewport; #endif bool bResult = false; if (World-\u0026gt;WorldType == EWorldType::Game || World-\u0026gt;WorldType == EWorldType::PIE) { ViewportClient = World-\u0026gt;GetGameViewport(); bResult = ViewportClient != nullptr; } #if WITH_EDITOR else if (FModuleManager::Get().IsModuleLoaded(NAME_LevelEditorName)) { FLevelEditorModule\u0026amp; LevelEditorModule = FModuleManager::GetModuleChecked\u0026lt;FLevelEditorModule\u0026gt;(NAME_LevelEditorName); if (TargetViewport.IsValid()) { ActiveLevelViewport = TargetViewport.Pin(); } else { ActiveLevelViewport = LevelEditorModule.GetFirstActiveLevelViewport(); } bResult = ActiveLevelViewport.IsValid(); } #endif if (bResult) { TSharedRef\u0026lt;SConstraintCanvas\u0026gt; FullScreenCanvas = SNew(SConstraintCanvas); FullScreenCanvasWidget = FullScreenCanvas; FullScreenCanvas-\u0026gt;AddSlot() .Offset(FMargin(0, 0, 0, 0)) .Anchors(FAnchors(0, 0, 1, 1)) .Alignment(FVector2D(0, 0)) [ SNew(SDPIScaler) .DPIScale(InDPIScale) [ Widget-\u0026gt;TakeWidget() ] ];\nif (ViewportClient) { ViewportClient-\u0026gt;AddViewportWidgetContent(FullScreenCanvas); } #if WITH_EDITOR else { check(ActiveLevelViewport.IsValid()); ActiveLevelViewport-\u0026gt;AddOverlayWidget(FullScreenCanvas); OverlayWidgetLevelViewport = ActiveLevelViewport; } #endif }\n1 2 return bResult; } 这里是我从VirtualCamera中将这个功能剥离出来并放到了一个插件中。 百度云链接：\n如何更改VirtualCamera的输入按键绑定 VirtualCamera共支持三种输入方式：鼠标点击、IOS端触屏、移动手柄。最终生产肯定不能用鼠标；采用IOS端触屏的方式需要配合一个IOS端的应用“Unreal Remote”，但是目前来看画面延迟很高，而且触屏的方式效率很低，不适用于影视行业。根据现场经验来看大多数摄影师都是使用各种外部设备来调整焦点、光圈以及录制和回放的。所以最终采用XBox360手柄，它的按键比较多，基本能够满足需求。画面传输方便采用的是“至迅”的无线图传。跟踪设备采用“Optitrack”。 总体来看逻辑可以分为两大块：UI和Vcam组件，基本就是UI和Vcam组件的数据通信。Vcam组件有两个重要的属性：“Output Provider”和“Modifier”。前者指定了输出设备，后者主要用来编写修改Vcam组件的逻辑。如果是UI和Vcam需要交互的，那么按键的输入绑定逻辑就会写在“Vcam2UI”中。如果不和UI交互的输入绑定逻辑就会写在“Modifier”中。UI相关的键位绑定是通过集合的方式（TMap和TArray）。 找到“VirtualCameraUserSettings.h”中的函数“InjectGamepadKeybinds()”。绑定关系如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 void UVirtualCameraUserSettings::InjectGamepadKeybinds() { ActionMappings = { {TEXT(\u0026#34;VirtualCamera_Home_EarSelection_Up\u0026#34;), EKeys::Gamepad_DPad_Up}, {TEXT(\u0026#34;VirtualCamera_Home_EarSelection_Down\u0026#34;), EKeys::Gamepad_DPad_Down}, {TEXT(\u0026#34;VirtualCamera_Home_EarSelection_Left\u0026#34;), EKeys::Gamepad_DPad_Left}, {TEXT(\u0026#34;VirtualCamera_Home_EarSelection_Right\u0026#34;), EKeys::Gamepad_DPad_Right}, {TEXT(\u0026#34;VirtualCamera_Home_Selection\u0026#34;), EKeys::Gamepad_FaceButton_Bottom}, {TEXT(\u0026#34;VirtualCamera_Home_FStop\u0026#34;), EKeys::Gamepad_LeftTrigger}, {TEXT(\u0026#34;VirtualCamera_Home_LensesMenu\u0026#34;), EKeys::Gamepad_RightTrigger}, {TEXT(\u0026#34;VirtualCamera_Home_Screenshot\u0026#34;), EKeys::Gamepad_RightShoulder}, {TEXT(\u0026#34;VirtualCamera_Home_ToggleStats\u0026#34;), EKeys::Gamepad_Special_Right}, {TEXT(\u0026#34;VirtualCamera_Home_ToggleFlight\u0026#34;), EKeys::Gamepad_FaceButton_Top}, {TEXT(\u0026#34;VirtualCamera_Home_ToggleRecord\u0026#34;), EKeys::Gamepad_LeftShoulder}, {TEXT(\u0026#34;VirtualCamera_Playback_SelectsSwitch\u0026#34;), EKeys::Gamepad_LeftShoulder}, {TEXT(\u0026#34;VirtualCamera_Playback_SelectClipsUp\u0026#34;), EKeys::Gamepad_DPad_Up}, {TEXT(\u0026#34;VirtualCamera_Playback_SelectClipsDown\u0026#34;), EKeys::Gamepad_DPad_Down}, {TEXT(\u0026#34;VirtualCamera_Playback_PlayPause\u0026#34;), EKeys::Gamepad_RightShoulder}, {TEXT(\u0026#34;VirtualCamera_Playback_Fullscreen\u0026#34;), EKeys::Gamepad_FaceButton_Top}, {TEXT(\u0026#34;VirtualCamera_Playback_MakeClipSelect\u0026#34;), EKeys::Gamepad_FaceButton_Left}, {TEXT(\u0026#34;VirtualCamera_Playback_TargetClip\u0026#34;), EKeys::Gamepad_FaceButton_Bottom}, {TEXT(\u0026#34;VirtualCamera_Playback_BackStart\u0026#34;), EKeys::Gamepad_DPad_Left}, {TEXT(\u0026#34;VirtualCamera_Playback_Close\u0026#34;), EKeys::Gamepad_Special_Right}, {TEXT(\u0026#34;VirtualCamera_Map_SwitchFilterSnapshotFlagLeft\u0026#34;), EKeys::Gamepad_DPad_Left}, {TEXT(\u0026#34;VirtualCamera_Map_SwitchFilterSnapshotFlagRight\u0026#34;), EKeys::Gamepad_DPad_Right}, {TEXT(\u0026#34;VirtualCamera_Map_Close\u0026#34;), EKeys::Gamepad_Special_Right}, {TEXT(\u0026#34;VirtualCamera_Map_TeleportHome\u0026#34;), EKeys::Gamepad_FaceButton_Left}, {TEXT(\u0026#34;VirtualCamera_Map_ToggleFullscreen\u0026#34;), EKeys::Gamepad_FaceButton_Top}, {TEXT(\u0026#34;VirtualCamera_Recording_InitiateRecord\u0026#34;), EKeys::Gamepad_LeftTrigger}, {TEXT(\u0026#34;VirtualCamera_Interaction_Undo\u0026#34;), EKeys::Gamepad_LeftShoulder}, {TEXT(\u0026#34;VirtualCamera_Interaction_Redo\u0026#34;), EKeys::Gamepad_RightShoulder}, {TEXT(\u0026#34;VirtualCamera_Interaction_SRTSwitchUp\u0026#34;), EKeys::Gamepad_DPad_Up}, {TEXT(\u0026#34;VirtualCamera_Interaction_SRTSwitchDown\u0026#34;), EKeys::Gamepad_DPad_Down}, {TEXT(\u0026#34;VirtualCamera_Interaction_XYZSwitchLeft\u0026#34;), EKeys::Gamepad_DPad_Left}, {TEXT(\u0026#34;VirtualCamera_Interaction_XYZSwitchRight\u0026#34;), EKeys::Gamepad_DPad_Right}, {TEXT(\u0026#34;VirtualCamera_Interaction_Close\u0026#34;), EKeys::Gamepad_Special_Right}, {TEXT(\u0026#34;VirtualCamera_Interaction_LocalWorldToggle\u0026#34;), EKeys::Gamepad_FaceButton_Top}, {TEXT(\u0026#34;VirtualCamera_Interaction_SelectSpawnNumpad\u0026#34;), EKeys::Gamepad_FaceButton_Bottom}, {TEXT(\u0026#34;VirtualCamera_Focus_CloseMenu\u0026#34;), EKeys::Gamepad_Special_Right}, {TEXT(\u0026#34;VirtualCamera_Focus_Selection\u0026#34;), EKeys::Gamepad_FaceButton_Bottom}, {TEXT(\u0026#34;VirtualCamera_Focus_ToggleAuto\u0026#34;), EKeys::Gamepad_FaceButton_Top}, {TEXT(\u0026#34;VirtualCamera_Playback_Back\u0026#34;), EKeys::Gamepad_FaceButton_Right}, {TEXT(\u0026#34;VirtualCamera_Map_Back\u0026#34;), EKeys::Gamepad_FaceButton_Right}, {TEXT(\u0026#34;VirtualCamera_Interaction_Back\u0026#34;), EKeys::Gamepad_FaceButton_Right}, {TEXT(\u0026#34;VirtualCamera_Settings_Close\u0026#34;), EKeys::Gamepad_FaceButton_Right}, {TEXT(\u0026#34;VirtualCamera_Focus_CloseMenu\u0026#34;), EKeys::Gamepad_FaceButton_Right}, {TEXT(\u0026#34;VirtualCamera_MotionAdjustments_Close\u0026#34;), EKeys::Gamepad_Special_Right}, {TEXT(\u0026#34;VirtualCamera_Settings_Close\u0026#34;), EKeys::Gamepad_Special_Right}, {TEXT(\u0026#34;VirtualCamera_Focus_AdjustSelectorDown\u0026#34;), EKeys::Gamepad_DPad_Up}, {TEXT(\u0026#34;VirtualCamera_Focus_AdjustSelectorUp\u0026#34;), EKeys::Gamepad_DPad_Down}, {TEXT(\u0026#34;VirtualCamera_Focus_ButtonSelect\u0026#34;), EKeys::Gamepad_FaceButton_Bottom}, {TEXT(\u0026#34;VirtualCamera_AnimationPreview_PlayPause\u0026#34;), EKeys::Gamepad_RightShoulder}, {TEXT(\u0026#34;VirtualCamera_AnimationPreview_Close\u0026#34;), EKeys::Gamepad_FaceButton_Right}, {TEXT(\u0026#34;VirtualCamera_AnimationPreview_BackStart\u0026#34;), EKeys::Gamepad_DPad_Left}, {TEXT(\u0026#34;VirtualCamera_AnimationPreview_Close\u0026#34;), EKeys::Gamepad_Special_Right}, {TEXT(\u0026#34;VirtualCamera_Reposition_ToggleTiltOffset\u0026#34;), EKeys::Gamepad_FaceButton_Left}, {TEXT(\u0026#34;VirtualCamera_Reposition_Close\u0026#34;), EKeys::Gamepad_Special_Right}, {TEXT(\u0026#34;VirtualCamera_Reposition_Close\u0026#34;), EKeys::Gamepad_FaceButton_Right}, {TEXT(\u0026#34;VirtualCamera_MotionAdjustments_MoveAxisUp\u0026#34;), EKeys::Gamepad_DPad_Up}, {TEXT(\u0026#34;VirtualCamera_MotionAdjustments_MoveAxisDown\u0026#34;), EKeys::Gamepad_DPad_Down}, {TEXT(\u0026#34;VirtualCamera_MotionAdjustments_OpenNumpad\u0026#34;), EKeys::Gamepad_FaceButton_Bottom}, {TEXT(\u0026#34;VirtualCamera_MotionAdjustments_SelectionSwitchLeft\u0026#34;), EKeys::Gamepad_LeftShoulder}, {TEXT(\u0026#34;VirtualCamera_MotionAdjustments_SelectionSwitchRight\u0026#34;), EKeys::Gamepad_RightShoulder}, {TEXT(\u0026#34;VirtualCamera_MotionAdjustments_LockAxis\u0026#34;), EKeys::Gamepad_FaceButton_Top}, {TEXT(\u0026#34;VirtualCamera_MotionAdjustments_ResetDutch\u0026#34;), EKeys::Gamepad_FaceButton_Left}, {TEXT(\u0026#34;VirtualCamera_MotionAdjustments_NumpadLeft\u0026#34;), EKeys::Gamepad_DPad_Left}, {TEXT(\u0026#34;VirtualCamera_MotionAdjustments_NumpadRight\u0026#34;), EKeys::Gamepad_DPad_Right}, {TEXT(\u0026#34;VirtualCamera_MotionAdjustments_Back\u0026#34;), EKeys::Gamepad_FaceButton_Right}, }; AxisMappings = { {TEXT(\u0026#34;VirtualCamera_Home_MoveFocalLengthSelection\u0026#34;), EKeys::Gamepad_RightY,1.0f}, {TEXT(\u0026#34;VirtualCamera_Home_MoveFStopSelection\u0026#34;), EKeys::Gamepad_LeftY,1.0f}, {TEXT(\u0026#34;VirtualCamera_Playback_FastScroll\u0026#34;), EKeys::Gamepad_LeftY,1.0f}, {TEXT(\u0026#34;VirtualCamera_Playback_FastScrub\u0026#34;), EKeys::Gamepad_RightX,1.0f}, {TEXT(\u0026#34;VirtualCamera_Playback_BackwardFrame\u0026#34;), EKeys::Gamepad_LeftTriggerAxis,1.0f}, {TEXT(\u0026#34;VirtualCamera_Playback_ForwardFrame\u0026#34;), EKeys::Gamepad_RightTriggerAxis,1.0f}, {TEXT(\u0026#34;VirtualCamera_Map_Zoom\u0026#34;), EKeys::Gamepad_RightY,1.0f}, {TEXT(\u0026#34;VirtualCamera_Map_XAxisPan\u0026#34;), EKeys::Gamepad_LeftX,1.0f}, {TEXT(\u0026#34;VirtualCamera_Map_YAxisPan\u0026#34;), EKeys::Gamepad_LeftY,1.0f}, {TEXT(\u0026#34;VirtualCamera_Interaction_Value_Change\u0026#34;), EKeys::Gamepad_LeftY,1.0f}, {TEXT(\u0026#34;VirtualCamera_Interaction_Value_Change\u0026#34;), EKeys::Gamepad_RightY,1.0f}, {TEXT(\u0026#34;VirtualCamera_Focus_ReticleMovementY\u0026#34;), EKeys::Gamepad_LeftY,1.0f}, {TEXT(\u0026#34;VirtualCamera_Focus_ReticleMovementX\u0026#34;), EKeys::Gamepad_LeftX,1.0f}, {TEXT(\u0026#34;VirtualCamera_Focus_AdjustFocalDistance\u0026#34;), EKeys::Gamepad_RightY,1.0f}, {TEXT(\u0026#34;VirtualCamera_AnimationPreview_FastScrub\u0026#34;), EKeys::Gamepad_RightX,1.0f}, {TEXT(\u0026#34;VirtualCamera_AnimationPreview_ForwardFrame\u0026#34;), EKeys::Gamepad_RightTriggerAxis,1.0f}, {TEXT(\u0026#34;VirtualCamera_AnimationPreview_BackwardFrame\u0026#34;), EKeys::Gamepad_LeftTriggerAxis,1.0f}, {TEXT(\u0026#34;VirtualCamera_MotionAdjustments_AdjustRadial\u0026#34;), EKeys::Gamepad_LeftY,1.0f}, {TEXT(\u0026#34;VirtualCamera_MotionAdjustments_AdjustRadial\u0026#34;), EKeys::Gamepad_RightY,1.0f}, {TEXT(\u0026#34;VirtualCamera_MotionAdjustments_AdjustRadial\u0026#34;), EKeys::Gamepad_LeftX,1.0f}, {TEXT(\u0026#34;VirtualCamera_MotionAdjustments_AdjustRadial\u0026#34;), EKeys::Gamepad_RightX,1.0f}, {TEXT(\u0026#34;VirtualCamera_Home_LeftMoveHorizontal\u0026#34;), EKeys::Gamepad_LeftX,1.0f}, {TEXT(\u0026#34;VirtualCamera_Home_RightMoveHorizontal\u0026#34;), EKeys::Gamepad_RightX,1.0f}, }; } 打开控件“Vcam2UI”，找到TMap“KeyBindingsHome”和“AxisBindingsHome”，通过更改Tmap中的Key值就能改变键位对应的函数关系。如图1-2所示。 图1-2\n","permalink":"https://imrcao.top/posts/%E5%BD%92%E6%A1%A3/ue4-virtualcamera2.0%E6%BA%90%E4%BB%A3%E7%A0%81%E6%B5%85%E6%9E%90/","summary":"VirtualCamera是虚幻引擎中的工程示例项目。虚幻引擎4.26版本中将其升级到了2.0（Beta版本），功能和UI都全面更新（如图1-1所示）。虚拟摄像机在虚幻引擎中驱动电影摄像机（Cine Camera），它能够将结果输出到各种外部设备中。我使用它主要用来拍摄镜头预演。 演示连接：https://www.bilibili.com/video/BV195411w75h/\n图1-1\nVirtualCamera如何在输出上覆盖自定义的UMG控件\n我这边采用的是“BlackMagic”采集卡（型号：）通过SDI线将画面传输到监视设备。根据引擎自带的“BlackMagic”插件是能够将画面采集并输出到监视设备的，但并不能将“UMG”控件采集并输出。“VirtualCamera”中可以实现这样的功能。下面简单分析一下这一块的逻辑是如何执行的。 UMG是在Vcam组件属性中OutputPrivider中指定的，打开源码找到对应的文件 VcamOutputProviderBase.h。简单阅读一遍后很容易就能发现逻辑是从初始化函数“Initialize()”开始执行的，经过一系列判断后一次执行“Acitvate()”、“CreateUMG()”和“DisplayUMG()”。具体代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 void UVCamOutputProviderBase::CreateUMG() { if (!","title":"UE4-VIrtualCamera2.0源代码浅析"},{"content":"目的：异步加载指定路径下的所有Texture2D资源，并将它们显示到WIdget的Image上，每隔一定时间更新一次图片。\n知识点包括：1、获取UI组件，2、获得场景中的Actor,3、实现异步批量加载资源，4、UE4定时器使用\n一、分别创建继承自UUserWidget和AActor的C++类，UWealthWidget和AWealthActor。 基于UWealthWidget创建一个蓝图UWealthWidget_BP,在蓝图控件中创建Image命名为TexImage。\n二、回到VS中，UWealthWidget.h中代码如下： 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #pragma once #include \u0026#34;CoreMinimal.h\u0026#34; #include \u0026#34;Blueprint/UserWidget.h\u0026#34; #include \u0026#34;WealthWidget.generated.h\u0026#34; class UImage; class UOverlay; UCLASS() class FRAMECOURSE_API UWealthWidget : public UUserWidget { GENERATED_BODY() public: //利用UE4反射机制获得UMG中的控件，这里的名字一定要和蓝图中创建的Image的名字一致。 UPROPERTY(Meta = (Bindwidget)) UImage* TexImage; public: //重载父类的初始化函数 virtual bool Initialize() override; //加载2D图片 void AssignTexture(UTexture2D* InTexture); }; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 UWealthWidget.cpp中代码如下： #include \u0026#34;WealthWidget.h\u0026#34; #include \u0026#34;Kismet/GameplayStatics.h\u0026#34; #include \u0026#34;Wealth/WealthActor.h\u0026#34; #include \u0026lt;Image.h\u0026gt; bool UWealthWidget::Initialize() { //这里一定要判断一下父类 if (!Super::Initialize()) return false; TArray\u0026lt;AActor*\u0026gt; ActArray; //获取场景中所有AWealthActor类型的Actor，并放到数组中 UGameplayStatics::GetAllActorsOfClass(GetWorld(), AWealthActor::StaticClass(), ActArray); if (ActArray.Num() \u0026gt; 0) { AWealthActor* wealthActor = Cast\u0026lt;AWealthActor\u0026gt;(ActArray[0]); //执行wealthActor中的方法，主要目的是将自己传出去，给wealthActor中的方法用，以便调用AssignTexture方法，来设置图片。 wealthActor-\u0026gt;AssignWealthWidget(this); } return true; } void UWealthWidget::AssignTexture(UTexture2D* InTexture) { TexImage-\u0026gt;SetBrushFromTexture(InTexture); } 三、在AWealthActor类中，主要实现：获取资源的软引用（路径引用）（硬引用是指指针引用）；实现异步加载资源；定时更新图片； 需要加载的资源： （1）获取资源的软引用（用类UObjectLibrary） .h\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 UObjectLibrary* ObjectLibrary; TArray\u0026lt;FSoftObjectPath\u0026gt; TexturePath; void AWealthActor::objectLibraryOperate() { if (!ObjectLibrary) { ObjectLibrary = UObjectLibrary::CreateLibrary(UObject::StaticClass(), false, false); //防止垃圾回收 ObjectLibrary-\u0026gt;AddToRoot(); } //加载指定路径下的所有资源到内存中 ObjectLibrary-\u0026gt;LoadAssetDataFromPath(TEXT(\u0026#34;/Game/Resource/UI/Texture/MenuTex\u0026#34;)); TArray\u0026lt;FAssetData\u0026gt; TextureData; //将路径下的所有资源数据放到数组TextureData中。 ObjectLibrary-\u0026gt;GetAssetDataList(TextureData); for (int32 i = 0; i \u0026lt; TextureData.Num(); ++i) { TexturePath.AddUnique(TextureData[i].ToSoftObjectPath()); } } （2）开始异步加载，实现定时执行功能\n//.h变量声明 //异步加载管理器 FStreamableManager* wealthLoaderManager; //异步加载句柄\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 TSharedPtr\u0026lt;FStreamableHandle\u0026gt; wealthHandle; //存放异步加载的资源 TArray\u0026lt;UTexture2D*\u0026gt; TextureGroup; //获得Widget指针 UWealthWidget* wealthWidget; //时间句柄 FTimerHandle UpdataTextureHandle; //循环加载图片的下标 int32 TextureIndex; void AWealthActor::StreamableManageerOperate() { //创建加载管理器 wealthLoaderManager = new FStreamableManager(); //请求执行异步加载，添加资源链接数组和加载完成后的回调 wealthHandle = wealthLoaderManager-\u0026gt;RequestAsyncLoad(TexturePath, FStreamableDelegate::CreateUObject(this,\u0026amp;AWealthActor::StreamableManagerLoadComplete)); } void AWealthActor::StreamableManagerLoadComplete() { //实现异步加载完成后动态修改图片 //存放异步加载的资源 TArray\u0026lt;UObject*\u0026gt;OutObject; //将 异步加载的资源放到数组里 wealthHandle-\u0026gt;GetLoadedAssets(OutObject); for (int32 i = 0; i\u0026lt; OutObject.Num(); ++i) { UTexture2D* workTexture = Cast\u0026lt;UTexture2D\u0026gt;(OutObject[i]); if (workTexture) TextureGroup.Add(workTexture); } FTimerDelegate UpdateTextureDele = FTimerDelegate::CreateUObject(this, \u0026amp;AWealthActor::UpdateTexture); GetWorld()-\u0026gt;GetTimerManager().SetTimer(UpdataTextureHandle, UpdateTextureDele, 0.5f, true); } void AWealthActor::UpdateTexture() { if (!wealthWidget) return; wealthWidget-\u0026gt;AssignTexture(TextureGroup[TextureIndex]); TextureIndex = (TextureIndex + 1) \u0026gt;= TextureGroup.Num() ? 0 : (TextureIndex + 1); } 四、回到UE4，将WealthWidget_BP添加到屏幕，将AWealthActor扔到场景中（或者创建一个它的蓝图），运行即可看到效果。 ","permalink":"https://imrcao.top/posts/%E5%BD%92%E6%A1%A3/ue4c++%E5%AE%9E%E7%8E%B0%E5%BC%82%E6%AD%A5%E6%89%B9%E9%87%8F%E5%8A%A0%E8%BD%BD%E8%B5%84%E6%BA%90/","summary":"目的：异步加载指定路径下的所有Texture2D资源，并将它们显示到WIdget的Image上，每隔一定时间更新一次图片。\n知识点包括：1、获取UI组件，2、获得场景中的Actor,3、实现异步批量加载资源，4、UE4定时器使用\n一、分别创建继承自UUserWidget和AActor的C++类，UWealthWidget和AWealthActor。 基于UWealthWidget创建一个蓝图UWealthWidget_BP,在蓝图控件中创建Image命名为TexImage。\n二、回到VS中，UWealthWidget.h中代码如下： 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #pragma once #include \u0026#34;CoreMinimal.h\u0026#34; #include \u0026#34;Blueprint/UserWidget.h\u0026#34; #include \u0026#34;WealthWidget.generated.h\u0026#34; class UImage; class UOverlay; UCLASS() class FRAMECOURSE_API UWealthWidget : public UUserWidget { GENERATED_BODY() public: //利用UE4反射机制获得UMG中的控件，这里的名字一定要和蓝图中创建的Image的名字一致。 UPROPERTY(Meta = (Bindwidget)) UImage* TexImage; public: //重载父类的初始化函数 virtual bool Initialize() override; //加载2D图片 void AssignTexture(UTexture2D* InTexture); }; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 UWealthWidget.","title":"20191004-UE4C++实现异步批量加载资源"},{"content":"一：Slate定制样式 （一）预期效果：用Slate写一个控件，它的样式所用的美术资源默认是从本地加载的，创建一个SlateWidgetStyle能够从项目中加载美术资源（Content文件夹下的资源）。 如下图所示，用slate写的控件可以像UMG一样很容易的设置美术资源。 （二）实现步骤：（开始前已经用Slate写好了一个控件） （1）、创建C++类：MySlateWidgetStyleContainerBase（类创建好后UE4会报错，不用管它，重启一下就好了） （2）、自己定义类的名称和路径后，在VS中打开\n1 2 在.h文件中，如下图： 后面创建 SlateWidgetStyle的时候，会选择下面这个类创建。 FMyWidgetStyle是一个结构体，如下图所示： 1 2 在.cpp中有结构体FMyWidgetStyle中函数的相关实现，如下图： （3）获取样式。slate样式制作好后，就是如何获取到FMyWidgetStyle。\n在负责生成具体控件的类中获取slate样式FMyWidgetStyle。（最后创建slate样式的时候，样式的名称一定要和这里的名称“MyWidgetStyle_BP”一样。而且需要在指定的目录下创建，具体的路径是在负责slate样式的类中指定的。）\n（4）在UE4插件自己生成的负责控件样式的类中，更改资源的加载方式，如下图所示： （一定要在指定的路径下创建slate样式）\n（5）、编译代码，启动程序。在UE4中指定的路径下创建slate Widget Style:单击右键-\u0026gt;选择UserWidget-\u0026gt;选择slate Widget style。在弹出来的对话框中选择，我们自己创建的C++样式类：UTaskWidgetStyle （6）最后自己添加美术资源进行测试： ","permalink":"https://imrcao.top/posts/%E5%BD%92%E6%A1%A3/slate%E5%AE%9A%E5%88%B6%E6%A0%B7%E5%BC%8F/","summary":"一：Slate定制样式 （一）预期效果：用Slate写一个控件，它的样式所用的美术资源默认是从本地加载的，创建一个SlateWidgetStyle能够从项目中加载美术资源（Content文件夹下的资源）。 如下图所示，用slate写的控件可以像UMG一样很容易的设置美术资源。 （二）实现步骤：（开始前已经用Slate写好了一个控件） （1）、创建C++类：MySlateWidgetStyleContainerBase（类创建好后UE4会报错，不用管它，重启一下就好了） （2）、自己定义类的名称和路径后，在VS中打开\n1 2 在.h文件中，如下图： 后面创建 SlateWidgetStyle的时候，会选择下面这个类创建。 FMyWidgetStyle是一个结构体，如下图所示： 1 2 在.cpp中有结构体FMyWidgetStyle中函数的相关实现，如下图： （3）获取样式。slate样式制作好后，就是如何获取到FMyWidgetStyle。\n在负责生成具体控件的类中获取slate样式FMyWidgetStyle。（最后创建slate样式的时候，样式的名称一定要和这里的名称“MyWidgetStyle_BP”一样。而且需要在指定的目录下创建，具体的路径是在负责slate样式的类中指定的。）\n（4）在UE4插件自己生成的负责控件样式的类中，更改资源的加载方式，如下图所示： （一定要在指定的路径下创建slate样式）\n（5）、编译代码，启动程序。在UE4中指定的路径下创建slate Widget Style:单击右键-\u0026gt;选择UserWidget-\u0026gt;选择slate Widget style。在弹出来的对话框中选择，我们自己创建的C++样式类：UTaskWidgetStyle （6）最后自己添加美术资源进行测试： ","title":"Slate定制样式"}]