[9] UE C++ Snake

思维导图

背景地图制作

创建瓦片集

角色素材

GameMode功能

游戏开始控制食物的生成

食物生成池(性能优化)

/*
 *形参如果是一个引用,且没有添加const关键字,代表实参想要借助形参修改值
 * param 是否指定生成时候的地址
 */
void ASnakeGameModeBase::SpawnFood(FVector& SpawnLocation)
{
   AFoodActor* Food = nullptr;
   SpawnLocation.X = FMath::RandRange(-7950.f, 7950.f);
   SpawnLocation.Y = 0;
   SpawnLocation.Z = FMath::RandRange(-7950.f, 7950.f);
   //Food=GetWorld()->SpawnActor<AFoodActor>(AFoodActor::StaticClass());
   Food = GetFoodInstance(); //没有参数的函数
   Food->SetActorLocation(SpawnLocation);
}

/*
 * 函数调用者如果想提供位置,就调用这个函数
 */
AFoodActor* ASnakeGameModeBase::GetFoodInstance()
{
   AFoodActor* Food = nullptr;
   //判断数组(存放已经需要Destroy也就是被吃掉的废弃食物的容器)中有没有元素,有,Food赋值为废弃食物的地址
   if (EatFoodPointerArray.Num())
   {
      Food = EatFoodPointerArray.Last(); //获取到数组中最后一个元素(废弃食物的地址)
      EatFoodPointerArray.RemoveAt(EatFoodPointerArray.Num() - 1); //因为废弃的食物已经被重新使用了,所以移除数组
      //Food->SetFoodSprite();//重置素材的方法1:直接获取到素材并设置
      Food->RenderFood->SetSprite(Food->GetSprite()); //重置素材的方法2:找到组件并调用函数,设置给组件素材
   }
   return Food ? Food : GetWorld()->SpawnActor<AFoodActor>(AFoodActor::StaticClass());
   //Food如果存在一个值,代表需要返回废弃食物,如果为nullptr,代表数组中已经没有废弃食物使用了,就重新生成一个
}

/*
 * 函数调用者如果不想提供位置,就调用这个函数
 */
AFoodActor* ASnakeGameModeBase::GetFoodInstance(FVector& SpawnLocation)
{
   AFoodActor* Food = nullptr;
   SpawnLocation.X = FMath::RandRange(-7950.f, 7950.f);
   SpawnLocation.Y = 0;
   SpawnLocation.Z = FMath::RandRange(-7950.f, 7950.f);

   //判断数组(存放已经需要Destroy也就是被吃掉的废弃食物的容器)中有没有元素,有,Food赋值为废弃食物的地址
   if (EatFoodPointerArray.Num())
   {
      Food = EatFoodPointerArray.Last(); //获取到数组中最后一个元素(废弃食物的地址)
      EatFoodPointerArray.RemoveAt(EatFoodPointerArray.Num() - 1); //因为废弃的食物已经被重新使用了,所以移除数组
      Food->SetActorLocation(SpawnLocation);
      //Food->SetFoodSprite();//重置素材的方法1:直接获取到素材并设置
      Food->RenderFood->SetSprite(Food->GetSprite()); //重置素材的方法2:找到组件并调用函数,设置给组件素材
   }
   return Food
             ? Food
             : GetWorld()->SpawnActor<AFoodActor>(AFoodActor::StaticClass(), SpawnLocation, FRotator::ZeroRotator);
}

 减少场景中食物的数量

void ASnakeGameModeBase::SubFood()
{
   CurrentFoodCounter--;
   if (CurrentFoodCounter <= 1950) //如果场景中的食物数量到达1970就重新生成食物
   {
      FVector SpawnLocation;
      for (int32 i = 0; i < 50; i++)
      {
         SpawnFood(SpawnLocation);
      }
      CurrentFoodCounter += 50;
   }
}

SnakePawn 蛇身蛇头

生成蛇的身体逻辑

//SnakePawn.cpp 
/*
 * SnakeBodyPointer 蛇生指针存在代表还有后续蛇身
 */
void ASnakePawn::SpawnBody()
{
	if (CurrentSnakeState != ESnakeState::Free)return;
	ASnakeBodyActor* SpawnBody = GetWorld()->SpawnActor<ASnakeBodyActor>(ASnakeBodyActor::StaticClass()); //生成
	SpawnBody->SetBodySkinIndex(SnakeHeadSkinIndex);
	SpawnBody->SetSpawnThisBodyHead(this);
	SpawnBodyArray.Add(SpawnBody);

	//找位置
	if (SnakeBodyPointer)
	{
		//头后有一节蛇身? 找第一节蛇身开始询问指针是否为空(身后有没有一节身体)
		SnakeBodyPointer->SetSnakeBody(SpawnBody);
	}
	else
	{ //设置第一节蛇尾
		SnakeBodyPointer = SpawnBody;
		//SpawnBody->SetActorLocation(GetActorLocation()+RenderSnakeHead->GetUpVector()*-60);
		SpawnBody->SetActorLocation(GetActorLocation());
	}
}

//SnakeBodyActor.cpp 链式调用查找最后一个尾巴位置
void ASnakeBodyActor::SetSnakeBody(ASnakeBodyActor* Body)
{   if(SnakeBodyPointer)//当前类实例化出的对象是不是身后还有一节蛇身?
	{
		SnakeBodyPointer->SetSnakeBody(Body);
	}else
	{
		SnakeBodyPointer=Body;
		//Body->SetActorLocation(GetActorLocation()+GetActorUpVector()*-45);
		Body->SetActorLocation(GetActorLocation());//出生的位置是直接放在了上一节的身上
	}
}

 蛇移动 , 蛇身跟随的逻辑

//数组:存储蛇头走过的每一个点(点越多,蛇身与蛇头之间的间距就更大)
//TArray<FVector> SnakeMovePositionArray;
//SnakePawn.cpp
void ASnakePawn::UpdateSnakeMove(float DeltaTime)
{
   if (CurrentSnakeState != ESnakeState::Free)return;
   FVector _SpeedAndDirection = RenderSnakeHead->GetUpVector() * DeltaTime * MoveSpeed;
   AddActorLocalOffset(_SpeedAndDirection);
   // 移动完成之后进行位置的监测(如果碰到上边界就执行死亡逻辑)
   if (GetActorLocation().Z >= 7990)
   {
          //修改蛇的状态为死亡
          ChangeSnakeState(ESnakeState::Dead);
       }
   if (SnakeBodyPointer)//存在代表有蛇尾
   {
      SnakeBodyPointer->SnakeMovePositionArray.Add(GetActorLocation()); //Tick每执行一次,就记录一次蛇头的位置
   }
}
//SnakeBodyActor.cpp
void ASnakeBodyActor::SnakeBodyFollowSnakeHead()
{
	if(SnakeMovePositionArray.Num()>=15)//蛇头如果已经走出去10帧 了
	{
		//当前一节身体跟上
		SetActorLocation(SnakeMovePositionArray[0]);
		//自己走一帧 告知自己的下一节身体当前的位置(向下一节身体数组中添加一个位置)
		if(SnakeBodyPointer)
		{
			SnakeBodyPointer->SnakeMovePositionArray.Add(SnakeMovePositionArray[0]);
		}
		//因为SnakeMovePositionArray[0]这个点已经使用完成(自己更新了位置,并将这个位置告知了下一节身体),这就是一个废弃的点了
		SnakeMovePositionArray.RemoveAt(0);//移除数组中下标为0 的值(坐标点)
	}
}

Interface

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "SnakeInterface.generated.h"

// This class does not need to be modified.
UINTERFACE(MinimalAPI)
class USnakeInterface : public UInterface
{
   GENERATED_BODY()
};

/**
 * 
 */
class SNAKE_API ISnakeInterface
{
   GENERATED_BODY()
public:
   UFUNCTION(/*BlueprintCallable,*/BlueprintNativeEvent)
   class ASnakePawn* GetSnake();
};

AISnakeActorComponent AI组件

//Pawn.cpp 判断当前对象是否AI , Ai则新建一个组件附着到上面
void ASnakePawn::BeginPlay()
{
   Super::BeginPlay();
   UGameplayStatics::GetPlayerController(GetWorld(), 0)->SetViewTarget(this); //切换相机为当前类的相机

   //对象生成之后就开始判断这个对象的类型(玩家控制的角色,非玩家控制的角色)
   if (Cast<APlayerController>(GetController()))
   {
      SnakeHeadFlipbook = LoadObject<UPaperFlipbook>(
         nullptr,TEXT("/Script/Paper2D.PaperFlipbook'/Game/Snake/Animation/SN3/PF_SN3.PF_SN3'"));
      if (SnakeHeadFlipbook)
         RenderSnakeHead->SetFlipbook(SnakeHeadFlipbook);
      SnakeHeadSkinIndex = 3;
   }
   else
   {
      SnakeHeadSkinIndex = FMath::RandRange(1, 3);
      SnakeHeadFlipbook = LoadObject<UPaperFlipbook>(
         nullptr,TEXT("/Script/Paper2D.PaperFlipbook'/Game/Snake/Animation/SN1/PF_SN1.PF_SN1'"));
      if (SnakeHeadFlipbook)
         RenderSnakeHead->SetFlipbook(SnakeHeadFlipbook);
      AISnake = NewObject<UAISnakeActorComponent>(this);
      AISnake->RegisterComponent();
   }

   SphereComponent->OnComponentBeginOverlap.AddDynamic(this, &ASnakePawn::OnComponentBeginOverlap);
   SphereComponent->OnComponentEndOverlap.AddDynamic(this, &ASnakePawn::OnComponentEndOverlap);
}
/*
.h
*/
// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "AISnakeActorComponent.generated.h"

UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class SNAKE_API UAISnakeActorComponent : public UActorComponent
{
	GENERATED_BODY()

public:	
	// Sets default values for this component's properties
	UAISnakeActorComponent();

protected:
	// Called when the game starts
	virtual void BeginPlay() override;

public:	
	// Called every frame
	virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
// 这个类的作用是扩展功能使用的

	//寻找扩展对象:当前组件类实例化出的对象是在哪个类下的成员
	class ASnakePawn* Owner;

	void CheckEnemy();//检测敌方
	//制定检测时间
	UPROPERTY()
    float CheckTimeOffset;//时间累加值

	
};

/*
.cpp
*/
// Fill out your copyright notice in the Description page of Project Settings.


#include "AISnakeActorComponent.h"

#include "SnakePawn.h"

// Sets default values for this component's properties
UAISnakeActorComponent::UAISnakeActorComponent()
{
   // Set this component to be initialized when the game starts, and to be ticked every frame.  You can turn these features
   // off to improve performance if you don't need them.
   PrimaryComponentTick.bCanEverTick = true;

   // ...
}


// Called when the game starts
void UAISnakeActorComponent::BeginPlay()
{   Super::BeginPlay();
    Owner= Cast<ASnakePawn>(GetOwner());//得到附着对象目标
   if(Owner)
   {
      UE_LOG(LogTemp, Log, TEXT("AIComponent==========="))
   }
}


// Called every frame
void UAISnakeActorComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{  Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
   if((CheckTimeOffset+=DeltaTime)>=0.3f)
   {
      CheckTimeOffset=0;
      CheckEnemy();
   }
}
void UAISnakeActorComponent::CheckEnemy()
{
   if(!Owner)return;//有没有主人
   if(Owner->CurrentSnakeState!=ESnakeState::Free)return;//主人是不是正常移动的状态
   TArray< FHitResult> OutHits;
   FCollisionShape Shape;
   Shape.SetSphere(58.f);
   bool bHit = Owner->GetWorld()->SweepMultiByChannel(OutHits,
   Owner->GetActorLocation(),Owner->GetActorLocation(),FQuat(0,0,0,0),  ECollisionChannel::ECC_Camera,Shape);
   if(bHit)
   {
      for(const auto& Hit:OutHits)
      {
         if(IsValid(Hit.GetActor()))
         {
            //判断是不是蛇(蛇头  蛇身)
            ISnakeInterface* Snake=Cast<ISnakeInterface>(Hit.GetActor());
            if(Snake) //碰到的是蛇
            {
               ASnakePawn* Pawn = Snake->Execute_GetSnake(Hit.GetActor());//调用函数,得到蛇头的地址
               //判断是不是自身(自身的蛇身)
               if(Pawn==Owner)
               {
                  //是自身
               }else
               {
                  //不是自身:告知主人,转头就跑
                  Owner->bAIRunaway=true;
                  FVector RunawayDir = Owner->GetActorLocation()-Pawn->GetActorLocation();
                  if(RunawayDir.Size()>=40.f)
                  {
                     RunawayDir.Normalize();
                     Owner->AIRunawayRotator = RunawayDir.Rotation();
                  }
                  else
                  {
                     Owner->ChangeSnakeState(ESnakeState::Dead);
                  }
                  
               }
            }
         }
      }
   }
}

注意点

自己类不用前向声明 , 其他类声明需要加 class

ue c++静态函数只能调用静态的属性

形参如果是一个引用 , 且没有添加const关键字 , 代表实参想要借助形参修改值

 如果 OnComponentBeginOverlap 没有触发查看有没有加 UFunction() 如果还没有 , 加个别名在改回去

静态属性定义 : 类内里面声明 , 类外面定义

在创建的时候 让子类持有父类的指针 ,这样子类可以直接操作父类的东西

在场景没运行阶段 , 使用Anywhere , 并在蓝图里面选中 , 让一个对象持有选中对象的指针

posted @ 2024-04-17 19:29  啊賢  阅读(5)  评论(0编辑  收藏  举报