home..

Farmobot

theme logo



Introduction

theme logo

Farmobot est un “Party Game” ou vous incarnez un agriculteur pilote de méchas. Travailler ensemble pour récolter efficacement vos champs tout en les protégeant contre les envahisseurs nuisibles.

Equipe

Quentin KERHELLO : Vehicle Artist & 3D Artist Marco LANDO : Art Director & Tech Artist Tao PECQUERIAUX : Lead & Environment Artist

Guillaume ROGÉ : Technical Game Designer Matis DUPERRAY : Sound Designer & Music Composer Timothé HUERRE : Creative Director & System Designer Antoine LEROUX : 3C’s Designer Théodore LABYT : Producer & Level Designer

Kevin DELCROIX : Gameplay & UI Programmer Jean-Baptiste OSES : Lead & Network Programmer

Mon travail

J’ai majoritairement passé mon temps sur le multijoueur, l’ia et les “smarts particules”, en effet dans farmobot la plupart des éléments intérractibles sont en fait des particules.

Index

Transmettre grâce aux textures

Dans farmobot, les joueurs vont en permanence interagir avec leurs champs et les ennemis. Afin de pouvoir créer une quantité massive de ces 2 éléments, nous avons décidé d’utiliser des systèmes de particules. Ces particules vont être créées à partir du nouveau système de particule de unreal, le Niagara System.

theme logo

Problématique : Comment peut-on réussir à créer des interactions entre des objets et les particules ?

Dans la version de unreal 5.1, il existe une option qui permet de gérer les collisions des particules avec un environnement. Malheureusement de simples collisions ne suffisent à gérer l’ensemble des demandes de mes game designers.

Nous avons donc mis en place un système permettant de transmettre certains types d’informations grâce à des textures. Niagara system nous donne la possibilité de pouvoir envoyer certaines informations à un système de particule. Après de nombreux tests, on remarque que dans la version actuelle du moteur, il est impossible d’envoyer plusieurs tableaux de données à une même particule. Mais il est possible de lui envoyer une texture. On peut donc passer par cette texture afin de lui envoyer toutes les informations nécessaires.

Tout d’abor d créons une texture. Plus petite est la taille de la texture, moins d’informations pourront être transmises, mais meilleur seront les performances. Créons ensuite une classe “DynamicTexture” qui dérive de AActor. Cette classe va nous permettre de modifier en temps réel une texture donnée en paramètre.


void ADynamicTexture::SetPixelRaw(int32 X, int32 Y, uint8 Red, uint8 Green, uint8 Blue, uint8 Alpha)
{
	// Get the pointer of the specified pixel
	uint8* Ptr = GetPointerToPixel(X, Y);

	// Set the pixel (note that linear color uses floats between 0..1, but a uint8 ranges from 0..255)
	SetPixelInternal(Ptr, Red , Green , Blue , Alpha );
	
}


void ADynamicTexture::SetPixelInternal(uint8*& Ptr, uint8 Red, uint8 Green, uint8 Blue, uint8 Alpha)
{
	// Pixels are stored in BGRA format
	*Ptr = Blue;
	*(Ptr + 1) = Green;
	*(Ptr + 2) = Red;
	*(Ptr + 3) = Alpha;
	
}


void ADynamicTexture::UpdateTexture()
{
	// Make sure the proxy and the texture is valid
	if (UpdateTextureRegionProxy.IsValid() && Texture)
	{
		// Update the texture's regions
		Texture->UpdateTextureRegions(
			0,											// Mip index
			1,											// Number of regions
			UpdateTextureRegionProxy.Get(),				// Region proxy
			TextureWidth * DYNAMIC_TEXTURE_BYTES_PER_PIXEL,	// Source data pitch
			DYNAMIC_TEXTURE_BYTES_PER_PIXEL,			// Bytes per pixel of source data
			PixelBuffer.Get()							// Buffer of pixels to set
		);
		
	}
}

Maintenant que l’on peut modifier un pixel de cette texture comme on le souhaite, on peut réfléchir aux différents types qu’on veut pouvoir transmettre aux particules. Dans cet article je vais prendre en exemple un vecteur 3. Cela correspond aux positions des différents joueurs de la partie.

#define POSITION_TO_PIXEL_SIZE 4;

void SetPositionInPixel(int OffSet , FVector Position)
{
    int XPos = OffSet*POSITION_TO_PIXEL_SIZE;
    XPos = XPos%GetWidth();
    int YPos = floor(OffSet/GetHeight());

    FLinearColor XYZ0 = FLinearColor(0,0,0,0);
    uint8 NegPos = 0;
    
    if(Position.X < 0)
    {
        NegPos = ((1 << 0) | NegPos);
    }

    if(Position.Y < 0)
    {
        NegPos = ((1 << 1) | NegPos);
    }

    if(Position.Z < 0)
    {
        NegPos = ((1 << 2) | NegPos);
    }
    
    const uint32 UPositionX = abs(Position.X);
    const uint32 UPositionY = abs(Position.Y);
    const uint32 UPositionZ = abs(Position.Z);
    
    SetPixelRaw(XPos,YPos,NegPos,NegPos,NegPos,NegPos);
    SetPixelRaw(XPos+1,YPos,UPositionX >> 24,UPositionX >> 16,UPositionX >> 8,UPositionX >> 0);
    SetPixelRaw(XPos+2,YPos,UPositionY >> 24,UPositionY >> 16,UPositionY >> 8,UPositionY >> 0);
    SetPixelRaw(XPos+3,YPos,UPositionZ >> 24,UPositionZ >> 16,UPositionZ >> 8,UPositionZ >> 0);
    
    UpdateTexture();
}

void SetPositionAndFloatInPixel(int OffSet , FVector Position, int DataToImport8Bits, int DataToImport16Bits)
{
    int XPos = OffSet*POSITION_TO_PIXEL_SIZE;
    XPos = XPos%GetWidth();
    int YPos = floor(OffSet/GetHeight());
    
    uint8 NegPos = 0;
    
    if(Position.X < 0)
    {
        NegPos = ((1 << 0) | NegPos);
    }

    if(Position.Y < 0)
    {
        NegPos = ((1 << 1) | NegPos);
    }

    if(Position.Z < 0)
    {
        NegPos = ((1 << 2) | NegPos);
    }
    
    const uint32 UPositionX = abs(Position.X);
    const uint32 UPositionY = abs(Position.Y);
    const uint32 UPositionZ = abs(Position.Z);
    
    SetPixelRaw(XPos,YPos,NegPos, DataToImport8Bits, DataToImport16Bits >> 8, DataToImport16Bits >> 0);
    SetPixelRaw(XPos+1,YPos,UPositionX >> 24,UPositionX >> 16,UPositionX >> 8,UPositionX >> 0);
    SetPixelRaw(XPos+2,YPos,UPositionY >> 24,UPositionY >> 16,UPositionY >> 8,UPositionY >> 0);
    SetPixelRaw(XPos+3,YPos,UPositionZ >> 24,UPositionZ >> 16,UPositionZ >> 8,UPositionZ >> 0);
    
    UpdateTexture();
}

void SetPositionAndFloatInPixelCoordonate(int X, int Y , FVector Position, int DataToImport8Bits, int DataToImport16Bits)
{
    int XPos = X*POSITION_TO_PIXEL_SIZE;
    XPos = XPos%GetWidth();
    int YPos = Y;
    
    uint8 NegPos = 0;
    
    if(Position.X < 0)
    {
        NegPos |= 1 << 0;
    }

    if(Position.Y < 0)
    {
        NegPos |= 1 << 1;
    }

    if(Position.Z < 0)
    {
        NegPos |= 1 << 2;
    }
    
    const uint32 UPositionX = abs(Position.X);
    const uint32 UPositionY = abs(Position.Y);
    const uint32 UPositionZ = abs(Position.Z);
    
    SetPixelRaw(XPos,YPos,NegPos, DataToImport8Bits, DataToImport16Bits >> 8, DataToImport16Bits >> 0);
    SetPixelRaw(XPos+1,YPos,UPositionX >> 24,UPositionX >> 16,UPositionX >> 8,UPositionX >> 0);
    SetPixelRaw(XPos+2,YPos,UPositionY >> 24,UPositionY >> 16,UPositionY >> 8,UPositionY >> 0);
    SetPixelRaw(XPos+3,YPos,UPositionZ >> 24,UPositionZ >> 16,UPositionZ >> 8,UPositionZ >> 0);
    
    UpdateTexture();
}

Ce morceau de code nous montre les limites existant de ce système. En effet, un Fvector contient 96 bits et chaque pixel ne peut contenir que des informations de 32 bits. On va donc utiliser plusieurs pixels à la suite pour contenir l’ensemble de l’information. On ne va garder que les entiers et utiliser un autre pixel pour contenir d’autres éléments comme la négation, ou tout autre int/float. On a donc utilisé dans notre cas, 4 pixels par joueur.

Dynamic Texture

On peut voir en direct les couleurs de la texture qui changent en fonction de l’emplacement des joueurs !

De la texture à la particule

Maintenant que l’on peut écrire des données sur une texture, il faut pouvoir reconstruire la donnée pour correspondre au type souhaité. Sur notre “particule système”, créons un nouveau module que l’on nommera : “Texture to array” qui va prendre en entrée la texture, et sortir tous les tableaux nécessaires aux bons fonctionnements des particules.

theme logo

A l’intérrieur de ce module créons un custom hlsl : theme logo Ce nouveau module va contenir le code suivant :

//Pixel calcul
float DeltaPixel = 1/SquareWidthInPixel;
float StartPointX = (DeltaPixel*3)-(DeltaPixel/2);
float StartPointY = (DeltaPixel)-(DeltaPixel/2);

float EndPointX = (1)-(DeltaPixel/2);
float EndPointY = (1)-(DeltaPixel/2);

//Size of Array
float MinDamage = 2;
float MaxDamage = 3;

//temp value for adding bit
uint4 KarpRawnTemp;

float2 UV = float2(StartPointX,StartPointY);
float4 PixelColor;

//Position
DataTexture.SampleTexture2D(UV,PixelColor);

uint4 BitShiftValue = uint4(24,16,8,0);

float3 Position = float3(0,0,0);


//Negate
DataTexture.SampleTexture2D(float2((DeltaPixel*(CurrentIndex*4+1))-(DeltaPixel/2),(DeltaPixel)-(DeltaPixel/2)),PixelColor);

float negateX = 1;
float negateY = 1;
float negateZ = 1;

uint isBitSet = PixelColor[0]*255.f;
//uint isBitSet = PixelColor[0];

if((isBitSet>>0)&1)
{
    negateX = -1.f;
}

if((isBitSet>>1)&1)
{
    negateY = -1.f;
}

if((isBitSet>>2)&1)
{
    negateZ = -1.f;
}


//Xposition
DataTexture.SampleTexture2D(float2((DeltaPixel*(CurrentIndex*4+2))-(DeltaPixel/2),(DeltaPixel)-(DeltaPixel/2)),PixelColor);
KarpRawnTemp = PixelColor*255.f;
KarpRawnTemp = KarpRawnTemp << BitShiftValue;
Position[0] = KarpRawnTemp[0] + KarpRawnTemp[1] + KarpRawnTemp[2] + KarpRawnTemp[3];
Position[0] *= negateX;

//YPosition
DataTexture.SampleTexture2D(float2((DeltaPixel*(CurrentIndex*4+3))-(DeltaPixel/2),(DeltaPixel)-(DeltaPixel/2)),PixelColor);
KarpRawnTemp = PixelColor*255.f;
KarpRawnTemp = KarpRawnTemp << BitShiftValue;
Position[1] = KarpRawnTemp[0] + KarpRawnTemp[1] + KarpRawnTemp[2] + KarpRawnTemp[3];
Position[1] *= negateY;

//ZPosition
DataTexture.SampleTexture2D(float2((DeltaPixel*(CurrentIndex*4+4))-(DeltaPixel/2),(DeltaPixel)-(DeltaPixel/2)),PixelColor);
KarpRawnTemp = PixelColor*255.f;
KarpRawnTemp = KarpRawnTemp << BitShiftValue;
Position[2] = KarpRawnTemp[0] + KarpRawnTemp[1] + KarpRawnTemp[2] + KarpRawnTemp[3];
Position[2] *= negateZ;

PositionInPixel = Position;

On obtient finalement une sortie correspondant à une position (PositionInPixel) qui va être ensuite ajoutée à un tableau de position depuis la niagara system. Ces valeurs vont être ensuite utilisées pour vérifier par exemple, la position du joueur, ou la position de ces outils. Cette méthode de transmission d’informations est ensuite utilisée un peu partout dans le jeu. (Couper, brûler, arroser, récolter le champ , détruire, ralentir et aspirer les ennemies)

Des ias et des particules

Afin de rendre plus intelligentes nos particules, nous avons mis en place des machines à états pour chacune de ces particules. Il existe des états communs à un système de particules, et des états spécifiques à chaque particule.

Prenons un exemple simple, ou chaque particule possède 1 seul état, les champs. Chaque particule peut être dans 6 états différents :

On va créer un module pour chaque état et modifier certains paramètres en fonctions de l’état souhaité. Prenons un exemple simple :

theme logo

Ce module va vérifier un énum, et changer certains bool. Cela va permettre aux différents modules suivants de connaître l’état de chaque plante.

Prenons un exemple un peu plus complexe, nos ennemis. En effet Il existe 2 machines à états, une qui est liée au système, et l’autre qui existe pour chaque particule. La première liée au système permet de créer un comportement global à toutes les particules, on l’utilise pour faire bouger toutes les particules dans la bonne direction, mais une fois que la particule est à une certaine distance du champ, on change l’état de la particule. C’est très utile pour changer l’animation d’une particule sans changer le comportement global. f theme logo

Conclusion

J’ai beaucoup aimé travailler sur ce projet, j’ai pu découvrir des nouvelles manières d’aborder un problème grâce aux particules, et j’ai pu aborder le multijoueur sur unreal ! Merci de votre lecture !

© 2024 Oses Jean-Baptiste   •  Powered by Soopr   •  Theme  Moonwalk