[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 , 并在蓝图里面选中 , 让一个对象持有选中对象的指针