前言:
在游戏开发的过程中,我们不乏对应设计模式的运用,甚至有些人看到一些源码,知道这是干嘛的,却依旧不知道他这使用的“美妙”所在。这时候,你就需要深入抓设计模式了,它其中的涉及到的深度,令人着迷。例如:在游戏开发过程中,我们有些时候会对游戏的过程中错误的操作进行回撤,这时候为什么不考虑一下命令模式。想必再次之前,大家已经熟悉命令模式了吧,当然,熟不熟悉作为一篇博文,还是要描述的。
命令模式,简而言之就是将命令封装成一个类,像AI对Pawn发送命令一样,存在收发者。由于命令模式其解耦性质,所以在游戏开发模式中运用很广泛,当然,你要是了解过UE的自定义命令,这时候,你也会知道,UE这种规模庞大的引擎,都会选择用其作为底层框架搭建。因此,虚幻在处理回撤时候就会很方便。
平常我们写简单的输入测试时候,可能会这样处理:
void InputHandler::handleInput()
{
if (isPressed(BUTTON_Space)) jump();
else if (isPressed(BUTTON_G)) fireGun();
else if (isPressed(BUTTON_F)) PickUp();
else if (isPressed(BUTTON_W)) MoveTo();
}
这样处理,就将你的输入耦合到其中,不妨猜想,你是准备每帧的处理。接着,我们简单的用命令模式来进行操作:
class Command
{
public:
virtual ~Command() {}
virtual void execute(AActor& actor) = 0;
};
我们可以再execute中处理自己的方法
class InputHandle
{
public:
Command* BUTTON_Space;
Command* BUTTON_G;
Command* BUTTON_F;
Command* BUTTON_W;
};
Command* InputHandler::handleInput()
{
if (isPressed(BUTTON_Space)) return BUTTON_Space;
else if (isPressed(BUTTON_G)) return BUTTON_G;
else if (isPressed(BUTTON_F)) return BUTTON_F;
else if (isPressed(BUTTON_W)) return BUTTON_F;
return NULL;
}
这时候,我们只需要返回对应的命令,为空的时候,则表示没有命令。如果采用这种方式,我们在处理Crtl+Z这种回撤操作时候,也会更简单,这时候,我们只需要在Command类中添加一个UnDo的方法,里面写好属于自己的回撤方法就行。
class Command
{
public:
virtual ~Command() {}
virtual void execute(AActor& actor) = 0;
virtual void undo(AActor& actor) = 0;
};
我们可以记录每条执行的命令,当需要消息回撤时候,我们可以调用存储命令的栈,然后调用其中的undo方法,已达到回撤的目的,当然,这种回撤操作设置最大存储命令,什么时候需要清理命令,就根据你们自身的项目处理即可。
接下来让我们来看看UE中对命令的处理方式:
在我们检索Command之后,发现在配置文件有一大堆类似这种格式的配置文件,却不知道它是干嘛的,但是仔细一阅读,不妨让我们看出了一些端倪,我们这这时候回去猜想,虚幻引擎也是用命令模式搭建的,因此,它的这些不同按键去绑定了一行行的命令。想到这,想必你也有思路继续向下检索。
假设我们要对拖拽物体时候按End物体会放在地面上并贴合这种操作感兴趣,仔细想想,这个是在编辑器时候的命令,那我们不妨检索一下EditorCommand,这时候你会发现一个类叫FPListEditorCommands,它的注释也告诉你这是绑定编辑器按键的类。
class FPListEditorCommands : public TCommands{ public: FPListEditorCommands() : TCommands ("PListEditor", NSLOCTEXT("PListEditor", "PListEditor", "PList Editor"), NAME_None, FEditorStyle::GetStyleSetName()) { } TSharedPtr NewCommand; TSharedPtr OpenCommand; TSharedPtr SaveCommand; TSharedPtr SaveAsCommand; TSharedPtr DeleteSelectedCommand; TSharedPtr MoveUpCommand; TSharedPtr MoveDownCommand; TSharedPtr AddDictionaryCommand; TSharedPtr AddArrayCommand; TSharedPtr AddStringCommand; TSharedPtr AddBooleanCommand; virtual void RegisterCommands() override { UI_COMMAND( NewCommand, "New", "Creates a new plist file", EUserInterfaceActionType::Button, FInputChord(EModifierKey::Control, EKeys::N) ); UI_COMMAND( OpenCommand, "Open", "Opens an existing plist file", EUserInterfaceActionType::Button, FInputChord(EModifierKey::Control, EKeys::O) ); UI_COMMAND( SaveCommand, "Save", "Saves the current plist file", EUserInterfaceActionType::Button, FInputChord(EModifierKey::Control, EKeys::S) ); UI_COMMAND( SaveAsCommand, "Save As", "Saves the current plist file to a specific location", EUserInterfaceActionType::Button, FInputChord(EModifierKey::Control | EModifierKey::Alt, EKeys::S) ); UI_COMMAND( DeleteSelectedCommand, "Remove Selected", "Removed the selected entries from the plist", EUserInterfaceActionType::Button, FInputChord(EModifierKey::Control, EKeys::R) ); UI_COMMAND( MoveUpCommand, "Move Up", "Moves the selected entry up within its parent", EUserInterfaceActionType::Button, FInputChord(EModifierKey::Control, EKeys::U) ); UI_COMMAND( MoveDownCommand, "Move Down", "Moves the selected entry down within its parent", EUserInterfaceActionType::Button, FInputChord(EModifierKey::Control, EKeys::D) ); UI_COMMAND( AddDictionaryCommand, "Add Dictionary", "Adds a new dictionary to the selected file or array", EUserInterfaceActionType::Button, FInputChord()); UI_COMMAND( AddArrayCommand, "Add Array", "Adds a new array to the selected file, array, or dictionary", EUserInterfaceActionType::Button, FInputChord()); UI_COMMAND( AddStringCommand, "Add String", "Adds a new string to the selected file, array, or dictionary", EUserInterfaceActionType::Button, FInputChord()); UI_COMMAND( AddBooleanCommand, "Add Boolean", "Adds a new boolean to the selected file, array, or dictionary", EUserInterfaceActionType::Button, FInputChord()); } };
这时候,我们可以看到平常一些按键所绑定的快捷键
引入眼帘的快捷键的绑定FInputChord(EModifierKey::Control, EKeys::D)),仔细想想,我们的那个键是End键,那么我们不妨检索EKeys::End试试。
这时候,我们发现对应的End按键的命令还是有好些个,让我们简单的看看。
这就有我们平常跳开启模拟时候跳到最后一帧的操作
在我们大致翻动的过程中发现了这条命令:
UI_COMMAND( SnapToFloor, "Snap to Floor", "Snaps the actor or component to the floor below it", EUserInterfaceActionType::Button, FInputChord( EKeys::End ) );
这句话Snaps the actor or component to the floor below it翻译过来,不就是将将Actor或组件捕捉到其下方的地面,不就是我们想要的找操作吗,虽然命令找到了,可是有些时候我们想要对应的方法咋办,别急,接着往下看:
我们接着检索这个命令的名字,发现大致还是有好几处在使用:
简单看看第一行:
这不就是我们平常绑定的方法的写法吗,接着看下去,切进SnapToFloor_Clicked,我们发现了这层还不是我们想要的
void FLevelEditorActionCallbacks::SnapToFloor_Clicked( bool InAlign, bool InUseLineTrace, bool InUseBounds, bool InUsePivot )
{
const FScopedTransaction Transaction( NSLOCTEXT("UnrealEd", "SnapActorsToFloor", "Snap Actors To Floor") );
SnapTo_Clicked( InAlign, InUseLineTrace, InUseBounds, InUsePivot );
}
继续看SnapTo_Clicked中:
void FLevelEditorActionCallbacks::SnapTo_Clicked( const bool InAlign, const bool InUseLineTrace, const bool InUseBounds, const bool InUsePivot, AActor* InDestination )
{
// Fires ULevel::LevelDirtiedEvent when falling out of scope.
FScopedLevelDirtied LevelDirtyCallback;
bool bSnappedComponents = false;
// Let the component visualizers try to handle the selection.
if (GUnrealEd->ComponentVisManager.HandleSnapTo(InAlign, InUseLineTrace, InUseBounds, InUsePivot, InDestination))
{
bSnappedComponents = true;
}
// Gather the selected actors and components to later add to the ingore list for object snapping
TArray ObjectsToIgnore;
for (FSelectedEditableComponentIterator It(GEditor->GetSelectedEditableComponentIterator()); It; ++It)
{
USceneComponent* SceneComponent = Cast(*It);
ObjectsToIgnore.Add(FActorOrComponent(SceneComponent));
}
for (FSelectionIterator It(GEditor->GetSelectedActorIterator()); It; ++It)
{
AActor* Actor = Cast(*It);
ObjectsToIgnore.Add(FActorOrComponent(Actor));
}
if( !bSnappedComponents && GEditor->GetSelectedComponentCount() > 0 )
{
for(FSelectedEditableComponentIterator It(GEditor->GetSelectedEditableComponentIterator()); It; ++It)
{
USceneComponent* SceneComponent = Cast(*It);
if(SceneComponent)
{
SceneComponent->Modify();
AActor* ActorOwner = SceneComponent->GetOwner();
bSnappedComponents = true;
if(ActorOwner)
{
ActorOwner->Modify();
GEditor->SnapObjectTo(FActorOrComponent(SceneComponent), InAlign, InUseLineTrace, InUseBounds, InUsePivot, FActorOrComponent(InDestination), ObjectsToIgnore);
ActorOwner->InvalidateLightingCache();
ActorOwner->UpdateComponentTransforms();
LevelDirtyCallback.Request();
}
}
}
USceneComponent* LastComp = GEditor->GetSelectedComponents()->GetBottom();
GEditor->SetPivot(LastComp->GetComponentLocation(), false, true);
}
if( !bSnappedComponents )
{
for(FSelectionIterator It(GEditor->GetSelectedActorIterator()); It; ++It)
{
AActor* Actor = Cast(*It);
if(Actor)
{
Actor->Modify();
GEditor->SnapObjectTo(FActorOrComponent(Actor), InAlign, InUseLineTrace, InUseBounds, InUsePivot, FActorOrComponent(InDestination), ObjectsToIgnore);
Actor->InvalidateLightingCache();
Actor->UpdateComponentTransforms();
LevelDirtyCallback.Request();
}
}
AActor* Actor = GEditor->GetSelectedActors()->GetBottom();
if(Actor)
{
GEditor->SetPivot(Actor->GetActorLocation(), false, true);
if(UActorGroupingUtils::IsGroupingActive())
{
// set group pivot for the root-most group
AGroupActor* ActorGroupRoot = AGroupActor::GetRootForActor(Actor, true, true);
if(ActorGroupRoot)
{
ActorGroupRoot->CenterGroupLocation();
}
}
}
}
GEditor->RedrawLevelEditingViewports();
}
我们终于找到了按End键执行的操作了,到此,想必大家对应UE的命令也稍微熟悉很多了吧,当我们创建插件时候窗口插件时候,UE也会自动给我们生成对应的Command的类,到此,文章不再继续深入,感兴趣的小伙伴,可以自己大致看看。



