
Здравствуйте меня зовут Дмитрий. Я занимаюсь созданием компьютерных игр на Unreal Engine в качестве хобби.
Как вы знаете недавно вышла Mirror's edge 2. Судя по отзывам критиков игра получилась очень слабая. И вы наверно уже захотели сделать свой Mirror's edge. Поэтому сегодня я расскажу как создать компонент движения, чтобы ваш персонаж двигался как героиня Mirror's edge.
Здесь будет описано создание компонента движения (дальше КД) который позволит персонажу:
1) Запрыгивать на стену.
2) Бегать по стене.
3) Перескакивать через небольшие препятствия
4) Делать ускорение при непрерывном беге
5)Делать подкат при нажатие Shift
6) Скатываться с наклонных поверхностей
7) А также мы создадим интерактивный объект веревку по которой тоже можно будет скатится
Все исходники приведены в конце статьи.
Если вы впервые слышите о компоненте движения, то вам лучше всего пройти урок Epic Games там все очень подробно расписано.
В качестве базового объекта для КД я использовал UCharacterMovementComponent это компонент движения уже позволяет персонажу ходить плавать и летать.
UCLASS() class CLIMBINGSYSTEM_API UClimbingPawnMovementComponent : public UCharacterMovementComponent { GENERATED_UCLASS_BODY() public: UFUNCTION(BlueprintCallable, Category = "ClimbingMovement") void SetClimbMode(EClimbingMode _ClimbingMode); UFUNCTION(BlueprintPure, Category = "ClimbingMovement") EClimbingMode GetClimbingMode() const; UFUNCTION(BlueprintPure, Category = "ClimbingMovement") bool CanSetClimbMode(EClimbingMode ClimbingMode); /*Offset from top of climbing surfase*/ UPROPERTY(Category = "ClimbingMovement|Climb", EditAnywhere, BlueprintReadWrite) int32 ClimbDeltaZ; /*Velocyty of climb movement*/ UPROPERTY(Category = "ClimbingMovement|Climb", EditAnywhere, BlueprintReadWrite) float ClimbVelocyty; /*Velocyty of jump from climb state*/ UPROPERTY(Category = "ClimbingMovement|Climb", EditAnywhere, BlueprintReadWrite) float ClimbJumpVelocyty; /*Angle from center when state can change in degres*/ UPROPERTY(Category = "ClimbingMovement|WallRun", EditAnywhere, BlueprintReadWrite) float WallRunLimitAngle; /*Offset from Wall when Wall Run*/ UPROPERTY(Category = "ClimbingMovement|WallRun", EditAnywhere, BlueprintReadWrite) int32 WallOffset; /*Fall Gravity Scale when charecter run on wall*/ UPROPERTY(Category = "ClimbingMovement|WallRun", EditAnywhere, BlueprintReadWrite) float WallRunFallGravityScale; /*Multiplier input vector when charecter run on wall*/ UPROPERTY(Category = "ClimbingMovement|WallRun", EditAnywhere, BlueprintReadWrite) int32 WallRunInputVelocyty; /*Velocyty of jump near wall*/ UPROPERTY(Category = "ClimbingMovement|WallRun", EditAnywhere, BlueprintReadWrite) float WallRunJumpZVelocyty; /*Velocyty of jump from wall run state*/ UPROPERTY(Category = "ClimbingMovement|WallRun", EditAnywhere, BlueprintReadWrite) float WallRunJumpVelocyty; /*Offset from rope of zip line*/ UPROPERTY(Category = "ClimbingMovement|ZipLine", EditAnywhere, BlueprintReadWrite) int32 ZipLineDeltaZ; /*Velocyty*/ UPROPERTY(Category = "ClimbingMovement|ZipLine", EditAnywhere, BlueprintReadWrite) float ZipLineVelocyty; /*Velocyty of jump from Zip Line state*/ UPROPERTY(Category = "ClimbingMovement|ZipLine", EditAnywhere, BlueprintReadWrite) float ZipLineJumpVelocyty; /*Angle of surfase when character slide*/ UPROPERTY(Category = "ClimbingMovement|InclinedSlide", EditAnywhere, BlueprintReadWrite) float InclinedSlideAngle; UPROPERTY(Category = "ClimbingMovement|InclinedSlide", EditAnywhere, BlueprintReadWrite) float InclinedSlideVelosytyForward; UPROPERTY(Category = "ClimbingMovement|InclinedSlide", EditAnywhere, BlueprintReadWrite) float InclinedSlideVelosytyRight; UPROPERTY(Category = "ClimbingMovement|InclinedSlide", EditAnywhere, BlueprintReadWrite) float InclinedJumpVelocyty; /*Velocyty of Run movement*/ UPROPERTY(Category = "ClimbingMovement", EditAnywhere, BlueprintReadWrite) float RunSpeed; /*delay before Run movement in sec*/ UPROPERTY(Category = "ClimbingMovement", EditAnywhere, BlueprintReadWrite) float RunDelay; /*Velocyty of jump near wall*/ UPROPERTY(Category = "ClimbingMovement", EditAnywhere, BlueprintReadWrite) float UnderWallJumpZVelocyty; UPROPERTY(Category = "ClimbingMovement", EditAnywhere, BlueprintReadWrite) FRuntimeFloatCurve SlideVelocytyCurve; /*UCharacterMovementComponent Interfase*/ virtual bool DoJump(bool bReplayingMoves) override; virtual float GetMaxSpeed() const override; virtual void BeginPlay() override; virtual void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) override; private: EClimbingMode ClimbingMode; EClimbingMode LastClimbingMode; bool BlockClimb; bool BlockWallRun; bool BlockInclinedSlide; FTimerHandle RunTimerHandle; bool bIsRun; float MinSlideTime; float MaxSlideTime; FTimerHandle InclinedSlideTimerHandle; void SetRun(); void DefineClimbMode(); bool SetMode(EClimbingMode ClimbingMode); void UnSetMode(EClimbingMode ClimbingMode); void UnBlockInclinedSlide(); void UnblockClimbState(); void UnblockWallRunState(); bool CheckDeltaVectorInCurrentState(const FVector& InputDeltaVector, FVector& CheckDeltaVector, FRotator& CheckRotation); //Check climb is possibly from Approximate coordinate and return realy coordinate bool CheckDeltaVectorInCurrentState(FVector& CheckDeltaVector, FRotator& CheckRotation); //Check climb is possibly in current character location coordinate and return realy coordinate bool CheckDeltaVectorInCurrentState();//Check climb is possibly in current character location without return new coordinates void MoveTo(const FVector& Delta, const FRotator& NewRotation); };
Итак как мы видим КД является конечным автоматом который в зависимости от значения переменной ClimbingMode определяет ту или иную модель поведения. А теперь про основные методы:
DoJump — Вызывается если персонаж прыгает.
TickComponent — Самая важная функция, вызывается каждый кадр. Здесь работает основной код.
SetClimbMode — Переключает КД из одного состояния в другое
DefineClimbMode — Определяет в какое состояние должен переключится КД.
CanSetClimbMode — определяет сможет ли КД переключится в нужное состояние в данный момент.
CheckDeltaVectorInCurrentState — Получает вектор на который должен переместится персонаж и если в новых координатах персонаж все ещё может находится в заданном состояние то возвращает true, уточненные координаты и углом поворота персонажа в противном случае false.
MoveTo — Перемещает персонажа в нужное положение.
Объект UCharacterMovementComponent работает только в паре с объектом ACharacter. Поэтому класс нашего персонажа будет производным от ACharacter вот собственно и он.
UCLASS() class CLIMBINGSYSTEM_API AClimbingCharacter : public ACharacter { GENERATED_BODY() public: UPROPERTY(Category = Character, VisibleAnywhere, BlueprintReadOnly) USpringArmComponent* CameraSpringArm; UPROPERTY(Category = Character, VisibleAnywhere, BlueprintReadOnly) UCameraComponent* Camera; // Sets default values for this pawn's properties //AClimbingCharacter(); AClimbingCharacter(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); // Called when the game starts or when spawned virtual void BeginPlay() override; // Called every frame virtual void Tick( float DeltaSeconds ) override; // Called to bind functionality to input virtual void SetupPlayerInputComponent(class UInputComponent* InputComponent) override; void MoveForward(float AxisValue); void MoveRight(float AxisValue); void CameraPitch(float AxisValue); void CameraYaw(float AxisValue); UFUNCTION(BlueprintCallable, Category = "Pawn|Character") virtual void Jump() override; UFUNCTION(BlueprintCallable, Category = "ClimbingCharacter") void ChangeView(bool FistPirson); void SwitchView(); void CrouchFunk(); void UnCrouchFunk(); virtual void NotifyActorBeginOverlap(AActor* OtherActor) override; private: /** Pointer to climbing movement component*/ UPROPERTY(Category = Character, VisibleAnywhere, BlueprintReadOnly, meta = (AllowPrivateAccess = "true")) class UClimbingPawnMovementComponent* ClimbingMovement; class AOverlapObject* OverlopObject; class AZipLine* ZipLine; bool bFistPirsonView; USkeletalMeshComponent* ClimbMesh; UCapsuleComponent* ClimbCapsule; friend class UClimbingPawnMovementComponent; };
И тут у нас возникает первая проблема. Дело в том что объект ACharacter порождает объект UCharacterMovementComponent в своем конструкторе. Но нам то нужен не UCharacterMovementComponent, а UClimbingPawnMovementComponent. Чтобы ACharacter породил UClimbingPawnMovementComponent нужно конструктор AClimbingCharacter изменить с такова:
AClimbingCharacter::AClimbingCharacter(const class FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) {
на такой:
AClimbingCharacter::AClimbingCharacter(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer.SetDefaultSubobjectClass<UClimbingPawnMovementComponent>(ACharacter::CharacterMovementComponentName)) {
Теперь ACharacter будет порождать то что нам нужно.
Это интересно: Вы наверно заметили слово Super, что оно означает? Слово Super заменят собой имя базового класса для объекта в котором его используют. То есть если я напишу Super::Tick() то это значит что я вызвал метод Tick из базового класса объекта.
Дальше я расскажу про основные методы:
Вас наверно привлек метод ChangeView. Да по нажатию кнопки F можно будет переключатся с вида от первого лица в вид от третьего. Хоть это и расходится с каноном.
Методы MoveForward, MoveRight, CameraPitch, CameraYaw, CrouchFunk, UnCrouchFunk, Jump отвечают за ввод с клавиатуры и мыши.
Метод NotifyActorBeginOverlap срабатывает когда персонаж пересекает какой то другой объект. Для всех интерактивных объектов я создал базовый класс AOverlapObject вот собственно и он:
UCLASS() class CLIMBINGSYSTEM_API AOverlapObject : public AActor { GENERATED_BODY() public: enum EClimbingMode GetObjectType() const; protected: UPROPERTY(Category = ObjectType, VisibleAnywhere, BlueprintReadOnly, meta = (AllowPrivateAccess = "true")) TEnumAsByte<enum EClimbingMode> ObjectType; };
Этот объект содержит в себе переменную типа EClimbingMode когда персонаж пересекает этот объект, его Компонент движения переключается в данное состояние.
Пока что я сделал только один интерактивный объект AZipLine это такая веревка за которую персонаж хватается и скатывается по ней вниз. Если вы играли в Mirror's edge, то вы поняли о чем я говорю.
UCLASS() class CLIMBINGSYSTEM_API AZipLine : public AOverlapObject { GENERATED_BODY() public: AZipLine(); virtual void OnConstruction(const FTransform& Transform) override; virtual void PostEditMove(bool bFinished) override; /** The main skeletal mesh associated with this Character (optional sub-object). */ UPROPERTY(Category = ZipLine, VisibleDefaultsOnly, BlueprintReadOnly) class UStaticMeshComponent* StartBase; UPROPERTY(Category = ZipLine, VisibleDefaultsOnly, BlueprintReadOnly) class UStaticMeshComponent* EndBase; UPROPERTY(Category = ZipLine, VisibleDefaultsOnly, BlueprintReadOnly) class USplineComponent* Spline; UPROPERTY(Category = ZipLine, VisibleDefaultsOnly, BlueprintReadOnly) USceneComponent* Pivot; UPROPERTY(Category = ZipLine, VisibleDefaultsOnly, BlueprintReadOnly) UBoxComponent* EndBox; UPROPERTY(Category = ZipLine, EditAnywhere, BlueprintReadOnly) UStaticMesh* RopeMesh; UPROPERTY(Category = ZipLine, EditAnywhere, BlueprintReadOnly) float SplineHeight; #if WITH_EDITORONLY_DATA UPROPERTY() class UArrowComponent* ArrowComponent; #endif protected: TArray<class USplineMeshComponent*> AddedSplineMeshComponents; void SetupSpline(); };
Метод OnConstruction аналогичен функции ConstructionScript в blueprint он также вызывается при перемещении эктора или его изменении. Правда есть небольшое отличие, если в блюпринте все добавленные объекты автоматически уничтожаются при каждом перестроении, то в c++ нужно самому уничтожать объекты.
Небольшая демонстрация:
Собственно на этом все как и обещал исходники. По многочисленным просьбам населения я разместил их на github.