教你用 C++ 在 Unreal 中为游戏增加实时音视频互动(下)

这些变量用来公职 blueprint asset 中相关的 UI 元素。这里最重要的是 BindWidget 元属性。通过将指向小部件的指针标记为 BindWidget,你可以在你的 C++类的 Blueprint 子类中创建一个同名的小部件,并在运行时从 C++中访问它。

同时,还要添加如下成员:

//EnterChannelWidget.h

...

UCLASS()
class AGORAVIDEOCALL_API UEnterChannelWidget : public UUserWidget
{
  GENERATED_BODY()

  ...

public:
  AVideoCallPlayerController* PlayerController = nullptr;

  TUniquePtr<VideoCall> VideoCallPtr;

  ...
};

添加 Constructor 和 Construct/Destruct 方法

//EnterChannelWidget.h

...

UCLASS()
class AGORAVIDEOCALL_API UEnterChannelWidget : public UUserWidget
{
  GENERATED_BODY()

public:

  ...

  UEnterChannelWidget(const FObjectInitializer& objectInitializer);

  void NativeConstruct() override;

  ...
};

//EnterChannelWidget.cpp

UEnterChannelWidget::UEnterChannelWidget(const FObjectInitializer& objectInitializer)
  : Super(objectInitializer)
{
}

void UEnterChannelWidget::NativeConstruct()
{
  Super::NativeConstruct();

  if (HeaderTextBlock)
    HeaderTextBlock->SetText(FText::FromString("Enter a conference room name"));

  if (DescriptionTextBlock)
    DescriptionTextBlock->SetText(FText::FromString("If you are the first person to specify this name, \
the room will be created and you will\nbe placed in it. \
If it has already been created you will join the conference in progress"));

  if (ChannelNameTextBox)
    ChannelNameTextBox->SetHintText(FText::FromString("Channel Name"));

  if (EncriptionKeyTextBox)
    EncriptionKeyTextBox->SetHintText(FText::FromString("Encription Key"));

  if (EncriptionTypeTextBlock)
    EncriptionTypeTextBlock->SetText(FText::FromString("Enc Type:"));

  if (EncriptionTypeComboBox)
  {
    EncriptionTypeComboBox->AddOption("aes-128");
    EncriptionTypeComboBox->AddOption("aes-256");
    EncriptionTypeComboBox->SetSelectedIndex(0);
  }

  if (JoinButton)
  {
    UTextBlock* JoinTextBlock = WidgetTree->ConstructWidget<UTextBlock>(UTextBlock::StaticClass());
    JoinTextBlock->SetText(FText::FromString("Join"));
    JoinButton->AddChild(JoinTextBlock);
    JoinButton->OnClicked.AddDynamic(this, &UEnterChannelWidget::OnJoin);
  }

  if (ContactsTextBlock)
    ContactsTextBlock->SetText(FText::FromString("agora.io Contact support: 400 632 6626"));

  if (BuildInfoTextBlock)
    BuildInfoTextBlock->SetText(FText::FromString(" "));
}

增加 Setter 方法

初始化 PlayerController 和 VideoCallPtr 变量

//EnterChannelWidget.h

...

UCLASS()
class AGORAVIDEOCALL_API UEnterChannelWidget : public UUserWidget
{
  GENERATED_BODY()

public:

  ...

  void SetVideoCallPlayerController(AVideoCallPlayerController* VideoCallPlayerController);

  void SetVideoCall(TUniquePtr<VideoCall> PassedVideoCallPtr);

  ...
};
//EnterChannelWidget.cpp

void UEnterChannelWidget::SetVideoCallPlayerController(AVideoCallPlayerController* VideoCallPlayerController)
{
  PlayerController = VideoCallPlayerController;
}

void UEnterChannelWidget::SetVideoCall(TUniquePtr<VideoCall> PassedVideoCallPtr)
{
  VideoCallPtr = std::move(PassedVideoCallPtr);
}

增加 BlueprintCallable方法

要对相应的按钮 "onButtonClick "事件做出反应。

//EnterChannelWidget.h

...

UCLASS()
class AGORAVIDEOCALL_API UEnterChannelWidget : public UUserWidget
{
  GENERATED_BODY()

public:

  ...

  UFUNCTION(BlueprintCallable)
    void OnJoin();

  ...
};
//EnterChannelWidget.cpp

void UEnterChannelWidget::OnJoin()
{
  if (!PlayerController || !VideoCallPtr)
  {
    return;
  }

  FString ChannelName = ChannelNameTextBox->GetText().ToString();

  FString EncryptionKey = EncriptionKeyTextBox->GetText().ToString();
  FString EncryptionType = EncriptionTypeComboBox->GetSelectedOption();

  SetVisibility(ESlateVisibility::Collapsed);

  PlayerController->StartCall(
    std::move(VideoCallPtr),
    ChannelName,
    EncryptionKey,
    EncryptionType);
}

增加 update 方法

//EnterChannelWidget.h

...

UCLASS()
class AGORAVIDEOCALL_API UEnterChannelWidget : public UUserWidget
{
  GENERATED_BODY()

public:

  ...

  void UpdateVersionText(FString newValue);

  ...
};

//EnterChannelWidget.cpp

void UEnterChannelWidget::UpdateVersionText(FString newValue)
{
  if (BuildInfoTextBlock)
    BuildInfoTextBlock->SetText(FText::FromString(newValue));
}

创建 VideoViewWidget C++ 类

VideoViewWidget是一个存储动态纹理并使用RGBA buffer 更新动态纹理的类,该类是从VideoCall OnLocalFrameCallback/OnRemoteFrameCallback函数中接收到的。

创建类和添加所需的 include


//VideoViewWidget.h

#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"

#include "Components/Image.h"

#include "VideoViewWidget.generated.h"
//VideoViewWidget.cpp

#include "EngineUtils.h"
#include "Engine/Texture2D.h"

#include <algorithm>

添加成员变量

  • Buffer:用于存储RGBA缓冲区、Width、Height和BufferSize的变量 - 视频帧的参数。
  • RenderTargetImage:允许你在UI中显示Slate Brush或纹理或材质的图像小部件。
  • RenderTargetTexture:动态纹理,我们将使用Buffer变量更新。
  • FUpdateTextureRegion2D:指定一个纹理的更新区域 刷子 - 一个包含如何绘制Slate元素的笔刷。我们将用它来绘制RenderTargetImage上的RenderTargetTexture。
//VideoViewWidget.h

...

UCLASS()
class AGORAVIDEOCALL_API UVideoViewWidget : public UUserWidget
{
  GENERATED_BODY()

public:
  UPROPERTY(BlueprintReadOnly, meta = (BindWidget))
    UImage* RenderTargetImage = nullptr;

  UPROPERTY(EditDefaultsOnly)
    UTexture2D* RenderTargetTexture = nullptr;

  UTexture2D* CameraoffTexture = nullptr;

  uint8* Buffer = nullptr;
  uint32_t Width = 0;
  uint32_t Height = 0;
  uint32 BufferSize = 0;
  FUpdateTextureRegion2D* UpdateTextureRegion = nullptr;

  FSlateBrush Brush;

  FCriticalSection Mutex;

  ...
};

覆盖 NativeConstruct() 方法

在NativeConstruct中,我们将用默认颜色初始化我们的图像。为了初始化我们的RenderTargetTexture,我们需要使用CreateTransient调用创建动态纹理(Texture2D)。然后分配BufferSize为Width * Height * 4的BufferSize(用于存储RGBA格式,每个像素可以用4个字节表示)。为了更新我们的纹理,我们可以使用UpdateTextureRegions函数。这个函数的输入参数之一是我们的像素数据缓冲区。这样,每当我们修改像素数据缓冲区时,我们就需要调用这个函数来使变化在纹理中可见。现在用我们的RenderTargetTexture初始化Brush变量,然后在RenderTargetImage widget中设置这个Brush。

//VideoViewWidget.h

...

UCLASS()
class AGORAVIDEOCALL_API UVideoViewWidget : public UUserWidget
{
  GENERATED_BODY()

public:

...

  void NativeConstruct() override;

  ...
};
//VideoViewWidget.cpp

void UVideoViewWidget::NativeConstruct()
{
  Super::NativeConstruct();

  Width = 640;
  Height = 360;

  RenderTargetTexture = UTexture2D::CreateTransient(Width, Height, PF_R8G8B8A8);
  RenderTargetTexture->UpdateResource();

  BufferSize = Width * Height * 4;
  Buffer = new uint8[BufferSize];
  for (uint32 i = 0; i < Width * Height; ++i)
  {
    Buffer[i * 4 + 0] = 0x32;
    Buffer[i * 4 + 1] = 0x32;
    Buffer[i * 4 + 2] = 0x32;
    Buffer[i * 4 + 3] = 0xFF;
  }
  UpdateTextureRegion = new FUpdateTextureRegion2D(0, 0, 0, 0, Width, Height);
  RenderTargetTexture->UpdateTextureRegions(0, 1, UpdateTextureRegion, Width * 4, (uint32)4, Buffer);

  Brush.SetResourceObject(RenderTargetTexture);
  RenderTargetImage->SetBrush(Brush);
}

覆盖 NativeDestruct() 方法

//VideoViewWidget.h

...

UCLASS()
class AGORAVIDEOCALL_API UVideoViewWidget : public UUserWidget
{
  GENERATED_BODY()

public:

  ...

  void NativeDestruct() override;

  ...
};
//VideoViewWidget.cpp

void UVideoViewWidget::NativeDestruct()
{
  Super::NativeDestruct();

  delete[] Buffer;
  delete UpdateTextureRegion;
}

覆盖 NativeTick() 方法

如果UpdateTextureRegion Width或Height不等于memember的Width Height值,我们需要重新创建RenderTargetTexture以支持更新的值,并像Native Construct成员一样重复初始化。否则只需用Buffer调用UpdateTextureRegions。

//VideoViewWidget.h

...

UCLASS()
class AGORAVIDEOCALL_API UVideoViewWidget : public UUserWidget
{
  GENERATED_BODY()

public:

  ...

  void NativeTick(const FGeometry& MyGeometry, float DeltaTime) override;

  ...
};
//VideoViewWidget.cpp

void UVideoViewWidget::NativeTick(const FGeometry& MyGeometry, float DeltaTime)
{
  Super::NativeTick(MyGeometry, DeltaTime);

  FScopeLock lock(&Mutex);

  if (UpdateTextureRegion->Width != Width ||
    UpdateTextureRegion->Height != Height)
  {
    auto NewUpdateTextureRegion = new FUpdateTextureRegion2D(0, 0, 0, 0, Width, Height);

    auto NewRenderTargetTexture = UTexture2D::CreateTransient(Width, Height, PF_R8G8B8A8);
    NewRenderTargetTexture->UpdateResource();
    NewRenderTargetTexture->UpdateTextureRegions(0, 1, NewUpdateTextureRegion, Width * 4, (uint32)4, Buffer);

    Brush.SetResourceObject(NewRenderTargetTexture);
    RenderTargetImage->SetBrush(Brush);

    //UClass's such as UTexture2D are automatically garbage collected when there is no hard pointer references made to that object.
    //So if you just leave it and don't reference it elsewhere then it will be destroyed automatically.

    FUpdateTextureRegion2D* TmpUpdateTextureRegion = UpdateTextureRegion;

    RenderTargetTexture = NewRenderTargetTexture;
    UpdateTextureRegion = NewUpdateTextureRegion;

    delete TmpUpdateTextureRegion;
    return;
  }

  RenderTargetTexture->UpdateTextureRegions(0, 1, UpdateTextureRegion, Width * 4, (uint32)4, Buffer);
}

增加 UpdateBuffer() 方法

通过调用来更新 Buffer 值。我们希望从 Agora SDK 线程接收到新的值。由于 UE4 的限制,我们将值保存到变量 Buffer 中,并在 NativeTick 方法中更新纹理,所以这里不调用UpdateTextureRegions。

//VideoViewWidget.h

...

UCLASS()
class AGORAVIDEOCALL_API UVideoViewWidget : public UUserWidget
{
  GENERATED_BODY()

public:

  ...

  void UpdateBuffer( uint8* RGBBuffer, uint32_t Width, uint32_t Height, uint32_t Size );
  void ResetBuffer();
  ...
};
//VideoViewWidget.cpp 

void UVideoViewWidget::UpdateBuffer(
  uint8* RGBBuffer,
  uint32_t NewWidth,
  uint32_t NewHeight,
  uint32_t NewSize)
{
  FScopeLock lock(&Mutex);

  if (!RGBBuffer)
  {
    return;
  }

  if (BufferSize == NewSize)
  {
    std::copy(RGBBuffer, RGBBuffer + NewSize, Buffer);
  }
  else
  {
    delete[] Buffer;
    BufferSize = NewSize;
    Width = NewWidth;
    Height = NewHeight;
    Buffer = new uint8[BufferSize];
    std::copy(RGBBuffer, RGBBuffer + NewSize, Buffer);
  }
}

void UVideoViewWidget::ResetBuffer()
{
  for (uint32 i = 0; i < Width * Height; ++i)
  {
    Buffer[i * 4 + 0] = 0x32;
    Buffer[i * 4 + 1] = 0x32;
    Buffer[i * 4 + 2] = 0x32;
    Buffer[i * 4 + 3] = 0xFF;
  }
}

创建 VideoCallViewWidget C++类

VideoCallViewWidget 类的作用是显示本地和远程用户的视频。我们需要两个 VideoViewWidget 小部件,一个用来显示来自本地摄像头的视频,另一个用来显示从远程用户收到的视频(假设我们只支持一个远程用户)。

创建类和添加所需的 include

像之前那样创建Widget C++类,添加所需的include。

//VideoCallViewWidget.h 

#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "Components/SizeBox.h"

#include "VideoViewWidget.h"

#include "VideoCallViewWidget.generated.h"
//VideoCallViewWidget.cpp

#include "Components/CanvasPanelSlot.h"

添加成员变量

//VideoCallViewWidget.h 

...

UCLASS()
class AGORAVIDEOCALL_API UVideoCallViewWidget : public UUserWidget
{
  GENERATED_BODY()

public:

  UPROPERTY(BlueprintReadOnly, meta = (BindWidget))
    UVideoViewWidget* MainVideoViewWidget = nullptr;

  UPROPERTY(BlueprintReadOnly, meta = (BindWidget))
    USizeBox* MainVideoSizeBox = nullptr;

  UPROPERTY(BlueprintReadOnly, meta = (BindWidget))
    UVideoViewWidget* AdditionalVideoViewWidget = nullptr;

  UPROPERTY(BlueprintReadOnly, meta = (BindWidget))
    USizeBox* AdditionalVideoSizeBox = nullptr;

public:
  int32 MainVideoWidth = 0;
  int32 MainVideoHeight = 0;

  ...
};

覆盖 NativeTick() 方法


//VideoCallViewWidget.h 

...

UCLASS()
class AGORAVIDEOCALL_API UVideoCallViewWidget : public UUserWidget
{
  GENERATED_BODY()

public:

  ...

  void NativeTick(const FGeometry& MyGeometry, float DeltaTime) override;

  ...
};
//VideoCallViewWidget.cpp

void UVideoCallViewWidget::NativeTick(const FGeometry& MyGeometry, float DeltaTime)
{
  Super::NativeTick(MyGeometry, DeltaTime);

  auto ScreenSize = MyGeometry.GetLocalSize();

  if (MainVideoHeight != 0)
  {
    float AspectRatio = 0;
    AspectRatio = MainVideoWidth / (float)MainVideoHeight;

    auto MainVideoGeometry = MainVideoViewWidget->GetCachedGeometry();
    auto MainVideoScreenSize = MainVideoGeometry.GetLocalSize();
    if (MainVideoScreenSize.X == 0)
    {
      return;
    }

    auto NewMainVideoHeight = MainVideoScreenSize.Y;
    auto NewMainVideoWidth = AspectRatio * NewMainVideoHeight;

    MainVideoSizeBox->SetMinDesiredWidth(NewMainVideoWidth);
    MainVideoSizeBox->SetMinDesiredHeight(NewMainVideoHeight);

    UCanvasPanelSlot* CanvasSlot = Cast<UCanvasPanelSlot>(MainVideoSizeBox->Slot);
    CanvasSlot->SetAutoSize(true);

    FVector2D NewPosition;
    NewPosition.X = -NewMainVideoWidth / 2;
    NewPosition.Y = -NewMainVideoHeight / 2;
    CanvasSlot->SetPosition(NewPosition);
  }
}

更新 UpdateMainVideoBuffer/UpdateAdditionalVideoBuffe

//VideoCallViewWidget.h 

...

UCLASS()
class AGORAVIDEOCALL_API UVideoCallViewWidget : public UUserWidget
{
  GENERATED_BODY()

public:

  ...

  void UpdateMainVideoBuffer( uint8* RGBBuffer, uint32_t Width, uint32_t Height, uint32_t Size);
  void UpdateAdditionalVideoBuffer( uint8* RGBBuffer, uint32_t Width, uint32_t Height, uint32_t Size);

  void ResetBuffers();
  ...
};
//VideoCallViewWidget.cpp

void UVideoCallViewWidget::UpdateMainVideoBuffer(
  uint8* RGBBuffer,
  uint32_t Width,
  uint32_t Height,
  uint32_t Size)
{
  if (!MainVideoViewWidget)
  {
    return;
  }
  MainVideoWidth = Width;
  MainVideoHeight = Height;
  MainVideoViewWidget->UpdateBuffer(RGBBuffer, Width, Height, Size);
}

void UVideoCallViewWidget::UpdateAdditionalVideoBuffer(
  uint8* RGBBuffer,
  uint32_t Width,
  uint32_t Height,
  uint32_t Size)
{
  if (!AdditionalVideoViewWidget)
  {
    return;
  }
  AdditionalVideoViewWidget->UpdateBuffer(RGBBuffer, Width, Height, Size);
}

void UVideoCallViewWidget::ResetBuffers()
{
  if (!MainVideoViewWidget || !AdditionalVideoViewWidget)
  {
    return;
  }
  MainVideoViewWidget->ResetBuffer();
  AdditionalVideoViewWidget->ResetBuffer();
}

创建 VideoCallWidget C++ 类

VideoCallWidget 类作为示例应用程序的音频/视频调用小部件。它包含以下控件,与蓝图资产中的UI元素绑定。

创建类和添加所需的include

像之前那样创建Widget C++类,添加必要的include和转发声明。


//VideoCallWidget.h
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"

#include "Templates/UniquePtr.h"
#include "Components/Image.h"
#include "Components/Button.h"
#include "Engine/Texture2D.h"

#include "VideoCall.h"

#include "VideoCallViewWidget.h"

#include "VideoCallWidget.generated.h"

class AVideoCallPlayerController;
class UVideoViewWidget;

//VideoCallWidget.cpp

#include "Kismet/GameplayStatics.h"
#include "UObject/ConstructorHelpers.h"
#include "Components/CanvasPanelSlot.h"

#include "VideoViewWidget.h"

#include "VideoCallPlayerController.h"

增加成员变量

//VideoCallWidget.h

...

UCLASS()
class AGORAVIDEOCALL_API UVideoCallWidget : public UUserWidget
{
  GENERATED_BODY()

public:
  AVideoCallPlayerController* PlayerController = nullptr;

public:
  UPROPERTY(BlueprintReadOnly, meta = (BindWidget))
    UVideoCallViewWidget* VideoCallViewWidget = nullptr;

  //Buttons
  UPROPERTY(BlueprintReadOnly, meta = (BindWidget))
    UButton* EndCallButton = nullptr;
  UPROPERTY(BlueprintReadOnly, meta = (BindWidget))
    UButton* MuteLocalAudioButton = nullptr;
  UPROPERTY(BlueprintReadOnly, meta = (BindWidget))
    UButton* VideoModeButton = nullptr;

  //Button textures
  int32 ButtonSizeX = 96;
  int32 ButtonSizeY = 96;
  UTexture2D* EndCallButtonTexture = nullptr;
  UTexture2D* AudioButtonMuteTexture = nullptr;
  UTexture2D* AudioButtonUnmuteTexture = nullptr;
  UTexture2D* VideomodeButtonCameraoffTexture = nullptr;
  UTexture2D* VideomodeButtonCameraonTexture = nullptr;

  TUniquePtr<VideoCall> VideoCallPtr;

  ...
};

初始化VideoCallWidget

为每个按钮找到asset图像,并将其分配到相应的纹理。然后用纹理初始化每个按钮。

//VideoCallWidget.h

...

UCLASS()
class AGORAVIDEOCALL_API UVideoCallWidget : public UUserWidget
{
  GENERATED_BODY()

public:

  ...

  UVideoCallWidget(const FObjectInitializer& ObjectInitializer);

  void NativeConstruct() override;
  void NativeDestruct() override;

private:
  void InitButtons();

  ...
};
//VideoCallWidget.cpp 

void UVideoCallWidget::NativeConstruct()
{
  Super::NativeConstruct();

  InitButtons();
}

void UVideoCallWidget::NativeDestruct()
{
  Super::NativeDestruct();

  if (VideoCallPtr)
  {
    VideoCallPtr->StopCall();
  }
}

UVideoCallWidget::UVideoCallWidget(const FObjectInitializer& ObjectInitializer)
  : Super(ObjectInitializer)
{
  static ConstructorHelpers::FObjectFinder<UTexture2D>
    EndCallButtonTextureFinder(TEXT("Texture'/Game/ButtonTextures/hangup.hangup'"));
  if (EndCallButtonTextureFinder.Succeeded())
  {
    EndCallButtonTexture = EndCallButtonTextureFinder.Object;
  }
  static ConstructorHelpers::FObjectFinder<UTexture2D>
    AudioButtonMuteTextureFinder(TEXT("Texture'/Game/ButtonTextures/mute.mute'"));
  if (AudioButtonMuteTextureFinder.Succeeded())
  {
    AudioButtonMuteTexture = AudioButtonMuteTextureFinder.Object;
  }
  static ConstructorHelpers::FObjectFinder<UTexture2D>
    AudioButtonUnmuteTextureFinder(TEXT("Texture'/Game/ButtonTextures/unmute.unmute'"));
  if (AudioButtonUnmuteTextureFinder.Succeeded())
  {
    AudioButtonUnmuteTexture = AudioButtonUnmuteTextureFinder.Object;
  }
  static ConstructorHelpers::FObjectFinder<UTexture2D>
    VideomodeButtonCameraonTextureFinder(TEXT("Texture'/Game/ButtonTextures/cameraon.cameraon'"));
  if (VideomodeButtonCameraonTextureFinder.Succeeded())
  {
    VideomodeButtonCameraonTexture = VideomodeButtonCameraonTextureFinder.Object;
  }
  static ConstructorHelpers::FObjectFinder<UTexture2D>
    VideomodeButtonCameraoffTextureFinder(TEXT("Texture'/Game/ButtonTextures/cameraoff.cameraoff'"));
  if (VideomodeButtonCameraoffTextureFinder.Succeeded())
  {
    VideomodeButtonCameraoffTexture = VideomodeButtonCameraoffTextureFinder.Object;
  }
}

void UVideoCallWidget::InitButtons()
{
  if (EndCallButtonTexture)
  {
    EndCallButton->WidgetStyle.Normal.SetResourceObject(EndCallButtonTexture);
    EndCallButton->WidgetStyle.Normal.ImageSize = FVector2D(ButtonSizeX, ButtonSizeY);
    EndCallButton->WidgetStyle.Normal.DrawAs = ESlateBrushDrawType::Type::Image;

    EndCallButton->WidgetStyle.Hovered.SetResourceObject(EndCallButtonTexture);
    EndCallButton->WidgetStyle.Hovered.ImageSize = FVector2D(ButtonSizeX, ButtonSizeY);
    EndCallButton->WidgetStyle.Hovered.DrawAs = ESlateBrushDrawType::Type::Image;

    EndCallButton->WidgetStyle.Pressed.SetResourceObject(EndCallButtonTexture);
    EndCallButton->WidgetStyle.Pressed.ImageSize = FVector2D(ButtonSizeX, ButtonSizeY);
    EndCallButton->WidgetStyle.Pressed.DrawAs = ESlateBrushDrawType::Type::Image;
  }
  EndCallButton->OnClicked.AddDynamic(this, &UVideoCallWidget::OnEndCall);

  SetAudioButtonToMute();
  MuteLocalAudioButton->OnClicked.AddDynamic(this, &UVideoCallWidget::OnMuteLocalAudio);

  SetVideoModeButtonToCameraOff();
  VideoModeButton->OnClicked.AddDynamic(this, &UVideoCallWidget::OnChangeVideoMode);

}

添加按钮纹理

在演示程序中找到目录Content/ButtonTextures(你不必打开项目,只需在文件系统中找到这个文件夹即可)。所有的按钮纹理都存储在那里。在你的项目内容中创建一个名为ButtonTextures的新目录,将所有的按钮图片拖放到那里,让它们在你的项目中可用。

添加Setters

//VideoCallWidget.h

...

UCLASS()
class AGORAVIDEOCALL_API UVideoCallWidget : public UUserWidget
{
  GENERATED_BODY()

  ...

public:
  void SetVideoCallPlayerController(AVideoCallPlayerController* VideoCallPlayerController);
  void SetVideoCall(TUniquePtr<VideoCall> PassedVideoCallPtr);

  ...
};

//VideoCallWidget.cpp

void UVideoCallWidget::SetVideoCallPlayerController(AVideoCallPlayerController* VideoCallPlayerController)
{
  PlayerController = VideoCallPlayerController;
}

void UVideoCallWidget::SetVideoCall(TUniquePtr<VideoCall> PassedVideoCallPtr)
{
  VideoCallPtr = std::move(PassedVideoCallPtr);
}

增加用来更新 view 的方法

//VideoCallWidget.h

...

UCLASS()
class AGORAVIDEOCALL_API UVideoCallWidget : public UUserWidget
{
  GENERATED_BODY()

  ...

private:

  void SetVideoModeButtonToCameraOff();
  void SetVideoModeButtonToCameraOn();

  void SetAudioButtonToMute();
  void SetAudioButtonToUnMute();

  ...
};
//VideoCallWidget.cpp

void UVideoCallWidget::SetVideoModeButtonToCameraOff()
{
  if (VideomodeButtonCameraoffTexture)
  {
    VideoModeButton->WidgetStyle.Normal.SetResourceObject(VideomodeButtonCameraoffTexture);
    VideoModeButton->WidgetStyle.Normal.ImageSize = FVector2D(ButtonSizeX, ButtonSizeY);
    VideoModeButton->WidgetStyle.Normal.DrawAs = ESlateBrushDrawType::Type::Image;

    VideoModeButton->WidgetStyle.Hovered.SetResourceObject(VideomodeButtonCameraoffTexture);
    VideoModeButton->WidgetStyle.Hovered.ImageSize = FVector2D(ButtonSizeX, ButtonSizeY);
    VideoModeButton->WidgetStyle.Hovered.DrawAs = ESlateBrushDrawType::Type::Image;

    VideoModeButton->WidgetStyle.Pressed.SetResourceObject(VideomodeButtonCameraoffTexture);
    VideoModeButton->WidgetStyle.Pressed.ImageSize = FVector2D(ButtonSizeX, ButtonSizeY);
    VideoModeButton->WidgetStyle.Pressed.DrawAs = ESlateBrushDrawType::Type::Image;
  }
}

void UVideoCallWidget::SetVideoModeButtonToCameraOn()
{
  if (VideomodeButtonCameraonTexture)
  {
    VideoModeButton->WidgetStyle.Normal.SetResourceObject(VideomodeButtonCameraonTexture);
    VideoModeButton->WidgetStyle.Normal.ImageSize = FVector2D(ButtonSizeX, ButtonSizeY);
    VideoModeButton->WidgetStyle.Normal.DrawAs = ESlateBrushDrawType::Type::Image;

    VideoModeButton->WidgetStyle.Hovered.SetResourceObject(VideomodeButtonCameraonTexture);
    VideoModeButton->WidgetStyle.Hovered.ImageSize = FVector2D(ButtonSizeX, ButtonSizeY);
    VideoModeButton->WidgetStyle.Hovered.DrawAs = ESlateBrushDrawType::Type::Image;

    VideoModeButton->WidgetStyle.Pressed.SetResourceObject(VideomodeButtonCameraonTexture);
    VideoModeButton->WidgetStyle.Pressed.ImageSize = FVector2D(ButtonSizeX, ButtonSizeY);
    VideoModeButton->WidgetStyle.Pressed.DrawAs = ESlateBrushDrawType::Type::Image;
  }
}

void UVideoCallWidget::SetAudioButtonToMute()
{
  if (AudioButtonMuteTexture)
  {
    MuteLocalAudioButton->WidgetStyle.Normal.SetResourceObject(AudioButtonMuteTexture);
    MuteLocalAudioButton->WidgetStyle.Normal.ImageSize = FVector2D(ButtonSizeX, ButtonSizeY);
    MuteLocalAudioButton->WidgetStyle.Normal.DrawAs = ESlateBrushDrawType::Type::Image;

    MuteLocalAudioButton->WidgetStyle.Hovered.SetResourceObject(AudioButtonMuteTexture);
    MuteLocalAudioButton->WidgetStyle.Hovered.ImageSize = FVector2D(ButtonSizeX, ButtonSizeY);
    MuteLocalAudioButton->WidgetStyle.Hovered.DrawAs = ESlateBrushDrawType::Type::Image;

    MuteLocalAudioButton->WidgetStyle.Pressed.SetResourceObject(AudioButtonMuteTexture);
    MuteLocalAudioButton->WidgetStyle.Pressed.ImageSize = FVector2D(ButtonSizeX, ButtonSizeY);
    MuteLocalAudioButton->WidgetStyle.Pressed.DrawAs = ESlateBrushDrawType::Type::Image;
  }
}

void UVideoCallWidget::SetAudioButtonToUnMute()
{
  if (AudioButtonUnmuteTexture)
  {
    MuteLocalAudioButton->WidgetStyle.Normal.SetResourceObject(AudioButtonUnmuteTexture);
    MuteLocalAudioButton->WidgetStyle.Normal.ImageSize = FVector2D(ButtonSizeX, ButtonSizeY);
    MuteLocalAudioButton->WidgetStyle.Normal.DrawAs = ESlateBrushDrawType::Type::Image;

    MuteLocalAudioButton->WidgetStyle.Hovered.SetResourceObject(AudioButtonUnmuteTexture);
    MuteLocalAudioButton->WidgetStyle.Hovered.ImageSize = FVector2D(ButtonSizeX, ButtonSizeY);
    MuteLocalAudioButton->WidgetStyle.Hovered.DrawAs = ESlateBrushDrawType::Type::Image;

    MuteLocalAudioButton->WidgetStyle.Pressed.SetResourceObject(AudioButtonUnmuteTexture);
    MuteLocalAudioButton->WidgetStyle.Pressed.ImageSize = FVector2D(ButtonSizeX, ButtonSizeY);
    MuteLocalAudioButton->WidgetStyle.Pressed.DrawAs = ESlateBrushDrawType::Type::Image;
  }
}

增加 OnStartCall 方法


//VideoCallWidget.h

...

UCLASS()
class AGORAVIDEOCALL_API UVideoCallWidget : public UUserWidget
{
  GENERATED_BODY()

public:

  ...

  void OnStartCall( const FString& ChannelName, const FString& EncryptionKey, const FString& EncryptionType );

  ...
};
//VideoCallWidget.cpp

void UVideoCallWidget::OnStartCall(
  const FString& ChannelName,
  const FString& EncryptionKey,
  const FString& EncryptionType)
{
  if (!VideoCallPtr)
  {
    return;
  }

  auto OnLocalFrameCallback = [this](
    std::uint8_t* Buffer,
    std::uint32_t Width,
    std::uint32_t Height,
    std::uint32_t Size)
  {
    VideoCallViewWidget->UpdateAdditionalVideoBuffer(Buffer, Width, Height, Size);
  };
  VideoCallPtr->RegisterOnLocalFrameCallback(OnLocalFrameCallback);

  auto OnRemoteFrameCallback = [this](
    std::uint8_t* Buffer,
    std::uint32_t Width,
    std::uint32_t Height,
    std::uint32_t Size)
  {
    VideoCallViewWidget->UpdateMainVideoBuffer(Buffer, Width, Height, Size);
  };
  VideoCallPtr->RegisterOnRemoteFrameCallback(OnRemoteFrameCallback);

  VideoCallPtr->StartCall(ChannelName, EncryptionKey, EncryptionType);
}

增加 OnEndCall方法


//VideoCallWidget.h

...

UCLASS()
class AGORAVIDEOCALL_API UVideoCallWidget : public UUserWidget
{
  GENERATED_BODY()

public:

  ...

  UFUNCTION(BlueprintCallable)
  void OnEndCall();

  ...
};

//VideoCallWidget.cpp 

void UVideoCallWidget::OnEndCall()
{
  if (VideoCallPtr)
  {
    VideoCallPtr->StopCall();
  }

  if (VideoCallViewWidget)
  {
    VideoCallViewWidget->ResetBuffers();
  }

  if (PlayerController)
  {
    SetVisibility(ESlateVisibility::Collapsed);
    PlayerController->EndCall(std::move(VideoCallPtr));
  }
}

增加 OnMuteLocalAudio 方法


//VideoCallWidget.h

...

UCLASS()
class AGORAVIDEOCALL_API UVideoCallWidget : public UUserWidget
{
  GENERATED_BODY()

public:

  ...

  UFUNCTION(BlueprintCallable)
  void OnMuteLocalAudio();

  ...
};

//VideoCallWidget.cpp

void UVideoCallWidget::OnMuteLocalAudio()
{
  if (!VideoCallPtr)
  {
    return;
  }
  if (VideoCallPtr->IsLocalAudioMuted())
  {
    VideoCallPtr->MuteLocalAudio(false);
    SetAudioButtonToMute();
  }
  else
  {
    VideoCallPtr->MuteLocalAudio(true);
    SetAudioButtonToUnMute();
  }
}

增加 OnChangeVideoMode方法

//VideoCallWidget.h

...

UCLASS()
class AGORAVIDEOCALL_API UVideoCallWidget : public UUserWidget
{
  GENERATED_BODY()

public:

  ...

  UFUNCTION(BlueprintCallable)
  void OnChangeVideoMode();

  ...
};

//VideoCallWidget.cpp

void UVideoCallWidget::OnChangeVideoMode()
{
  if (!VideoCallPtr)
  {
    return;
  }
  if (!VideoCallPtr->IsLocalVideoMuted())
  {
    VideoCallPtr->MuteLocalVideo(true);

    SetVideoModeButtonToCameraOn();
  }
  else
  {
    VideoCallPtr->EnableVideo(true);
    VideoCallPtr->MuteLocalVideo(false);

    SetVideoModeButtonToCameraOff();
  }
}

增加 Blueprint 类

确保C++代码正确编译。没有成功编译的项目,你将无法进行下一步的操作。如果你已经成功地编译了C++代码,但在虚幻编辑器中仍然没有看到所需的类,请尝试重新打开项目。

创建 BP_EnterChannelWidget Blueprint Asset。

创建一个 UEnterChannelWidget 的 Blueprint,右键点击内容,选择用户界面菜单并选择 Widget Blueprint。
image

更改这个新的用户小工具的类的父类。打开 Blueprint,会出现 UMG 编辑器界面,默认情况下 Designer 选项卡是打开的。点击图形按钮(右上角按钮),选择 “类设置”。在面板 "Details "中,点击下拉列表 “父类”,选择之前创建的C++ 类 UEnterChannelWidget。现在返回到设计页面。调色板窗口包含几种不同类型的小部件,你可以用它们来构造你的 UI 元素。找到 Text、Editable Text、Button 和 ComboBox(String)元素,然后将它们拖到工作区,如图中所示。然后进入 "EnterChannelWidget.h "文件中的 UEnterChannelWidget 的定义,查看成员变量的名称和对应的类型(UTextBlock、EditableTextBox、UButton和UComboBoxString)。返回到 BP_VideoCallWiewVidget 编辑器中,给你拖动的UI元素设置相同的名称。你可以通过点击元素并在 "详细信息 "面板中更改名称来完成。尝试编译蓝图。如果你忘了添加什么东西,或者在你的UserWidget类中出现了widget名称/类型不匹配的情况,你会出现一个错误。
image
保存到文件夹中,例如 /Content/Widgets/BP_EnterChannelWidget.uasset。

创建 BP_VideoViewWidget Asset。

image
设定图片的锚点
image

创建 BP_VideoCallViewWidget Asset

创建 BP VideoCallViewWidget Asset ,将父类设置为 UVideoCallViewWidget,并添加 BP VideoViewWidget 类型的 UI 元素MainVideoViewWidget 和ExtendedVideoViewWidget。同时添加 SizeBox 类型的 MainVideoSizeBox 和 AdditionalVideoSizeBox UI 元素。
image
创建 BP_VideoCallWidget Asset

创建BPVideoCallWidget Asset,将父类设置为UVideoCallWidget,在 Palette UI 元素BPVideoCallViewWidget 中找到并添加名称为VideoCallViewWidget,添加 EndCallButton、MuteLocalAudioButton 和 VideoModeButton 按钮。

image

创建 BP_VideoCallPlayerController blueprint asset

现在是创建 BPVideoCallPlayerPlayerController blueprint asset 的时候了,基于我们前面描述的 AVideoCallPlayerPlayerController 类,创建 BPVideoCallPlayerController 蓝图资产。

image

创建一个AVideoCallPlayerPlayerController的bluepringt。右键点击内容,按Add New按钮,选择Blueprint类,在窗口中选择父类,在Pick parent类进入All classes部分,找到VideoCallPlayerController类。

现在将我们之前创建的小部件分配给PlayerController,如下图所示。

image
将其保存到文件夹,例如 /Content/Widgets/BP_VideoCallPlayerController.uasset。

创建 BP_AgoraVideoCallGameModeBase Asset

创建一个 AVideoCallPlayerController 的 Blueprint,右键点击内容,按 Add New 按钮,选择 Blueprint 类,在 Pick parent class 窗口中选择 Game Mode Base Class。这是所有游戏模式的父类。

修改 GameMode

现在你需要设置你的自定义 GameMode 类和玩家控制器。到世界设置中,设置指定的变量:

image

指定项目的设置

进入 Edit->Project settings,打开 Maps & Modes。设定默认参数:

image

推荐阅读
相关专栏
SDK 教程
167 文章
本专栏仅用于分享音视频相关的技术文章,与其他开发者和声网 研发团队交流、分享行业前沿技术、资讯。发帖前,请参考「社区发帖指南」,方便您更好的展示所发表的文章和内容。