C++
Lenguaje de alto nivel con características de bajo nivel: que permite controlar la memoria de bajo nivel. Uno de los focos: programación orientada a objetos
Descargar el IDE
Compilar/Recompilar
es traducir los nodos o el lenguaje de programación a 1 y 0, es decir lenguaje máquina, para que la computadora pueda ejecutar el código
- Compilar: aprovecha archivos temporales de la compilación anterior shortcut: Ctrl+B
- Recompilar: borra los temporales y hace todo desde 0
si quieres recompilar dale al nombre del proyecto click derecho y Recompilar
Crear una clase de C++
Tools/New C++ Class
- elegir (por ejemplo) actor...
- darle un nombre y ubicación al "BP"
En el head se crean estas "variables":
Dónde encontrar las clases en el editor de UE
Instanciar clase de C++ como BP
Buscar la clase de C++ en
C++ Classes/nombre del proyecto
y click derecho/Create BP Class based on {nombre de la clase}
se creará en la carpeta Content
Archivos de C++
.h (.hpp .hxx .hh .h++) (archivos de encabezado, headers)
contiene los identificadores (nombres) de elementos
aquí se declaran variables
.cpp (archivos de cuerpo)
Siempre hay parejas de archivos que que se relacionan por su contenido
aquí se definen variables
Explicación del código inicial en una nueva clase
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Score.generated.h"
UCLASS()
class CPP_PROYECTOCURSO_API AScore : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
AScore();
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
public:
// Called every frame
virtual void Tick(float DeltaTime) override;
};
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Score.generated.h"
aquí se están importando cosas de otros lados
CoreMinimal.h: algunas funciones básicas
GameFramework/Actor.h es la clase padre, en este caso Actor
Score.generated.h son los binarios que se crearon al crear esta clase (Score)
así como la comunicación entre blueprints se hace (por ejemplo) con casteos, en C++ se tiene que importar la referencia de la otra clase, aquí en la cabecera
UCLASS()
class CPP_PROYECTOCURSO_API AScore : public AActor
{
GENERATED_BODY()
- UCLASS: Macro de UE (los macros siempre tienen todas sus letras mayúsculas) que indica que esta clase (AScore) puede ser usada en el sistema de reflexión, es decir se pueda usar en el editor, BP, etc.
- class {{NOMBRE DEL PROYECTO}} _ API
Este macro que maneja exportaciones/importaciones entre módulos en proyectos Unreal. Le dice al compilador que esta clase es parte del módulo (el proyecto se llama CPP_ProyectoCurso) - {{NombreDeLaClase}} : public {{ClasePadre}} nombre de la clase, también indica que está heredando de la clase AActor (todas los hijos de AActor deben comenzar con un "A" delante por se se llama AScore)
- GENERATED_BODY(): Le dice al motor que lo siguiente va en el Sistema de reflexion, que esta parte no la convierta en 1 y 0 sino que se quede "legible"
public:
// Sets default values for this actor's properties
AScore();
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
public:
// Called every frame
virtual void Tick(float DeltaTime) override;
public: es accesible desde cualquier parte
protected: solo es accesible desde esta clase o clases que hereden de esta (hijos de esta clase)
BeginPlay y Tick, son métodos, que ejecutan al inicio del juego y en cada frame, respectivamente
virtual significa que puede ser reescrito en clases hijas
override significa que
Expresiones
unidad básica sobre la que se construye los programas
Una expresión tiene 2 propiedades
- tipo de valor: tipo de valor que resulta al evaluar la expresión: integer, decimal, bool, etc.
- categoría de valor: indica si la expresión se resuelve en un valor, un objeto, una función
Statement
en español instrucción o enunciado
es la unidad básica de ejecución en C++
es una línea o bloque de código que manda una indicación para que el programa haga algo
diferentes statements
- Declaración: crea nuevos nombres para guardar valores a la memoria
- Jump: saltar de un punto a otro del programa (por ejemplo if, si no se cumple la condición se salta el codigo que no se usa)
- Expresión: opera de valores
- Compund: suma de expresiones que producen una más compleja
- Selection: permite ejecutar secciones concretas de código desestimando otras, ejemplo: un switch
- Interaction: evaluar la memoria de manera ordenada
- Try blocks: para evaluar el propio código y cazador posibles errores: debugging, por ejemplo cuando añadimos un breakpoint
Comentarios
Es una forma de dejar notas en el código, no forma parte del código a compilar
//esto es un comentario de una sola linea
/*
así se hace un comentario
de 2 o más lineas
*/
Variables
la representación de un valor en memoria
siempre tiene un nombre para que podemos acceder a ella
siempre son declaradas en el .h (header)
Constante
A diferencia de una variable, una constante es un valor que no cambia después de ser definido
un dato que no varia ni cambia nunca
2 tipos de constantes:
- constantes durante compilación
El valor se conoce en el momento que se compila el programa. Más rápida - constantes durante ejecución
El valor no cambia, pero solo se conoce cuando el programa corre, Puede depender de algo que pasa al iniciar el juego (como un valor leído de un archivo de configuración o una instancia de un objeto).
a = 2 es como un set
a == 2 se usa para preguntar si a=2? como en un if
White spaces
El compilador ignora el excedente de espacios (o tabulaciones)
en programación una tab es igual a 2 espacios
pero poner espacios en medio de un identificador (como en el tercer ejemplo) 👆 está mal
el identificador es valu e ✖
Keywords
Son palabras que no puedes usar como identificador, porque ya están tomadas por C++
así como wiggle en after effects, ya está tomado
sería el nombre de una variable...como registrar una marca en registro público...los nombre tienen que ser únicos, por eso hay algunos identificadores que ya están tomados por funciones y que no puedes usar)
Tipos de datos
- void: (solo para funciones, no para variables) tipo indeterminado, por ejemplo el Tick y el begin play, porque solo tienen una acción que realizar pero no tienen un tipo
una función que devuelve algo, tiene que tener parámetros (de entrada y de salida)
en el caso del void, NO devuelve parametros, solo ejecuta - char: guarda caracteres, los puede guardar aislados, por ejemplo una palabra o una frase, también números enteros.
podemos almacenar 256 números diferentes (se refiera a un rango) por ejemplo numeros entre 0-255 o - short: guarda números enteros ocupa 2 bytes de memoria
almacena hasta 65536 valores distinto - int: guarda numeros enteros, 4 bytes permite...un huevo más de números distintos
- long: guardar enteros muy grandes, 32 bits
- long long: guardar enteros de 64 bits (el doble del long) es demasiado caro
- bool: 1 byte, muy barata, 0/cualquier otro valor: false/true
por ejemplo 5 ,255, 7 todo es verdadero, el único que es false, es 0 - float
llamamos "literales" a los datos sin nombre, no se puede hacer referencia a ellos excepto cuando los usamos
Los tipos (de variable) escribirlo manualmente, no fiarse del predict (autocompletado) que a veces lo escribe mal
Signed vs Unsigned (variables)
signed y unsigned son "especificadores" se ponen delante del identificador (nombre de variable) como complementando
- signed: todos los datos son signed por defecto, acepta positivos y negativos
- unsigned: puede guardar decimales, pero solo almacena positivos
signed se refiere a "tiene signo" por lo que aceptará positivo o negativo
sizeof()
Overflow/Underflow
cuando se intenta guardar en una variable, un valor, que sale del rango, si es
255+1 = 0
Cuando se sobrepasa por arriba es overflow, cuando se sobrepasa por abajo es underflow
0-1 = 255
El overflow/underflow se tienen que evitar, porque no se comporta consistentemente
Variables
Ejemplos de declaración de variable
int demoint;
short demoshort;
float demofloat;
Diferencia entre Declarar vs Definir vs Inicializar vs Asignar variables
Declarar: Le dices al compilador que existe una variable, pero no reservas memoria todavía (si es solo declaración sin definición).
extern int Score;
Definir: Crea la variable sin un valor, pero aquí sí se reserva memoria para la variable. Si ya la declaraste antes, ahora la estás haciendo “real” en la memoria.
int Score;
Inicializar: Cuando defines la variable y le asignas un valor al mismo tiempo
int Score = 0;
Asignar: Cuando ya la has Definido previamente pero NO le habías asignado un valor, entonces lo haces ahora (más adelante en el código)
Score = 10;
el tipo de valor (int, bool, float, etc...) se debe indicar cuando declaras y defines la variable, NO cuando inicializas o asignas, porque se entendería que estás volviendo a definir el valor, daría error
si tu valor es numérico, por recomendación siempre setearle un 0 como valor por defecto, por que si no le das un valor c++ le dará un espacio en memoria, lo llena con algo pero no sabes qué, podría estar llenándolo con "basura".
Tipos de inicialización
Tipo | Sintaxis | Seguro | Notas |
---|---|---|---|
Copy Initialization | int x = 5; | ✅ | Clásica, pero puede hacer copia |
Direct Initialization | int x(5); | ✅ | Llama constructor directamente |
List Initialization | int x{5}; | ✅✅ | Moderna y más segura |
Value Initialization | int x{}; | ✅ | Inicializa a 0 o constructor |
Default Initialization | int x; | ❌ | Puede tener basura |
En el curso siempre usamos copy initialization |
Operadores aritméticos
declarar variables en el .h (header) debajo de public
//Variables
char demochar;
int demoint;
short demoshort;
float demofloat;
dar valor (o setear) variables en el .cpp debajo del //sets default values
aqui en demo int queremos setearle un nuevo valor
entonces en la ultima linea demoint = demoint + 1;
si se le quieres sumar solo una unidad se puede hacer esto
-
demoint++;
también si se quiere restar 1 -
demoint--;
si se quiere sumar varias variables -
demoint +=2;
aquí se le está sumando 2 -
demoint <= 1250;
aquí lo que está haciendo es comparar demoint con 1250, y devuelve un valor 0 o 1 (booleano) dependiendo si es verdadero o falso
Ternarios...
Operadores lógicos
recomendación: no usar la coma
atajo para compilar: Ctrl + Shift + B
PRINT STRING (tener a mano siempre)
GEngine->AddOnScreenDebugMessage(-1, 15.0f, FColor::Yellow, FString::Printf(TEXT("Some debug message")));
Funciones
serie de enunciados o statements que se ejecutan secuencialmente para ser reutilizadas
permite encapsulamiento de código
se debe declarar en .h (nombre, parámetros, tipo de retorno)
y definir en .cpp (el código que indica qué debe hacer)
al declarar la función, se tiene que poner el tipo (int por ej.), identificador (nombre de la Funcion), y entre paréntesis los parámetros a usar, a los cuales también se especifica su tipo
Declarar función (.h)
se declaran debajo de public
int FuncionEntera();
aquí ambos serían lo mismo, está creando una función que devuelve un valor entero (int) y también se le da un nombre (identificador) a la función
además dentro del () puede haber parámetros, estos serían los valores de entrada
int FuncionEntera(float parametro, int parametro2);
estos pueden ser de cualquier tipo y se tiene que poner el tipo e identificador igual que con una variable, si quieres poner varios parámetros, separarlos por comas
si vas a hacer una función que no devuelve nada, solamente ejecuta algo, sería un void y hay que especificarlo (el void solo se utiliza para funciones, nunca para variables)
void FuncionVacia(float parametro, int parametro2);
para que te cree la estructura de la función, puedes darle al screwdriver y a "Crear definición..."
esto lo que hará es abrir el .cpp y hacer ahí la estructura de la definición de función...
Definir función (.cpp)
la definición se puede escribir al final...
...entonces con el truco de screwdriver,te crea esta estructura para definir la función en el .cpp
el color verde agua refiere al nombre de la clase
Te escribe entre {} y un return 0; el return quiere decir que está devolviendo un valor, y el 0, es normalmente en programación significado de que se ejecutado bien el código
int ATutFunctions::FunctionEntera(float parametro, int parametro2)
{
return 0;
}
(si la función es un bool, el return tenemos que escribirlo como true o false)
Luego completar con lo que pasa dentro de la función, que en este caso sería multiplicar los dos parámetros, se puede guardar en una nueva variable, que en este caso se llama int resultado
y ponerle return...el resultado
int ATutFunctions::FuncionEntera (int parametro, int parametro 2) {
int resultado = parametro * parametro2;
return resultado;
}
esta es la forma menos óptima ☝, porque en este caso no es necesario guardar esta variable, entonces podríamos directamente poner lo que hace en el return, ej: 👇
int ATutFunctions::FuncionEntera(int parametro, int parametro2) {
return parametro*parametro2;
}
Llamar función (desde un Event)
OK ahora que ya declaramos (.h) y definimos (.cpp), la función existe, pero no se ejecuta porque no la hemos llamado (en BP es como si no estuviera en el event graph ni conectada a ningún nodo rojo de evento)
Para llamarla hay que escribirla, debajo de algún algún evento, como el BeginPlay, o el Tick
pero como ya la hemos definido (le hemos puesto qué tipo devuelve y qué tipo son sus parámetros) al llamarla solamente pondremos sus valores, ej:
void ATutFunctions::BeginPlay()
{
Super::BeginPlay();
FuncionEntera(2,3);
}
aquí está FuncionEntera, debajo del BeginPlay y especificando sus parámetros 👆
En este caso, según lo que se escribió al definir la función, multiplicará ambos parámetros
En este caso deben ser 2 integers, porque así se puso en la definición de la función
pero...aunque se está ejecutando la función, no se está guardando el resultado en ningún lado, sería bueno guardarlo en una variable, entonces la guardaremos en una que creamos previamente: demoint
de esta manera estamos asignando demoint
void ATutFunctions::BeginPlay()
{
Super::BeginPlay();
demoint = FuncionEntera(2,3);
}
Parámetros por defecto de una función
cuando se inicializa, en el .cpp (también se puede en el .h pero hacerlo en el .cpp es más ordenado)
se puede dar valores por defecto a los parámetros poniéndoles un = y un valor
int ATutFunctions::FuncionEntera(int parametro = 4, int parametro2 = 5){
return parametro * parametro2;
}
Scope local
C++ variable scope explained 🌎
Hblar de un scope básicamente es hablar de los diferentes graph en BP (un scope es un graph)...
una función no sabe qué ocurre dentro de otra
hay variables locales y globales
- locales: existen solo dentro de la función, se declaran dentro de esa función
- globales: se suelen declarar en el .h y son reconocibles dentro y fuera de cualquier función
para sacar algún valor de una función, puedes asignarle este valor a una variable global, así todo mundo podrá leerla
pero NO OLVIDAR que hay que llamar a la función, sino esa variable global seguirá teniendo su valor por defecto, porque la función existe pero no se está ejecutando...ejemplo...
tengo una función tipo void, que opera una multiplicación...quiero sacar el resultado de la función para usarlo en otro lado, pero que la función siga siendo void
int ATutFunctions::FuncionEntera(int parametro = 4, int parametro2 = 5){
demoint = parametro * parametro2;
}
aquí se está usando la variable demoint para guardar la operación de parámetro*parámetro2
, pero eso solo pasa dentro de la función, si quieres sacar ese valor, hay que llamar a la función
Overload
Pueden coexistir funciones con el mismo identificador, pero con diferentes firmas
el identificador es el "nombre" la firma es ...tipo que devuelve+firma+parámetros de entrada
así que cuidado con eso, puede ser útil para algunas cosas (Jaume dice que para gameplay) pero puede confundir, al volver a un proyecto después de tiempo y ves dos funciones que se llaman igual pero hacen cosas distintas, de preferencia siempre nombrarlas todo diferente
si tienes una función de 2 parámetros, y al llamarla solo quieres ponerle el primero, y que para el segundo use su valor por defecto, debes hacer así
FuncionEntera(2)
porque si lo haces así 👇
FuncionEntera(2, )
va a parecer que te has equivocado y el programa lo detectará como error
Ejemplo práctico:
Health es una variable (previamente declarada) a la que le estamos asignando un valor
este valor es el resultado de FuncionEntera (esta función, multiplica sus parámetros que en este caso es 2 y 3, el resultado es 6)
la segunda linea es otro ejemplo, en este caso es un booleano, que está comparando si Función entera, que en este caso además está multiplicado *3, resultaría 18...es mayor o igual que Helath, dependiendo si se cumple o no, el bool será true o false
Health -= FuncionEntera(2,3);
bool demobool = Health <= (FuncionEntera(2,3)*3)
PRIMERA LINEA
se está restando Health - resultado de FuncionEntera(2,3)...recordemos que programamos FuncionEntera para que multiplique sus argumentos, entonces FuncionEntera valdría 2*3=6
Si Health tenía un valor inicial de 10, tras la operación:
Health = Health - 6;
Health = 4;
SEGUNDA LINEA
Se está evaluando si Health
es menor o igual al resultado de FuncionEntera(2,3) * 3
.
Ya sabemos que FuncionEntera(2,3)
es 6, entonces 6 * 3 = 18
.
Como en la línea anterior dejamos Health = 4
, la comparación es:
4 <= 18 --> verdadero (true)
Por lo tanto, la variable booleana demobool
será igual a true
(es decir, su valor será 1).
Nomenclatura
- usar camelCase
- no usar puntos (.)
- sí se puede usar
_ guión bajo
- a las variables booleanas se les suele poner una b delante, sólo con booleanos, sólo en unreal
bool bIsMoving;
Otros conceptos
una firma: tipo, nombre, inputs (parámetros de entrada)
Control de Flujo
en C++ se suelen usar if y switch
siempre dentro de un evento, tipo begin play, porque si se pone fuera, no se ejecutará
IF
if (20 == 30){ //entre los () se pone la condición
//CODIGO
}
si la condición se cumple, ejecuta el "//CODIGO", sino, lo saltea y va a la siguiente línea
if inline
este es una versión del if en una sola linea, pero solamente puede ejecutar en caso el if sea verdadero, no se puede definir qué ejecuta si el if es falso, ejemplo...
if (20 == 30) example = true;
si la condición 20==30
se cumple, entonces seteará example (que es una variable booleana) como verdadero, en este caso como no se cumple, example mantendrá su valor anterior
Ejemplo haciendo un Print
copiar pegar esta linea:
GEngine->AddOnScreenDebugMessage(-1, 15.0f, FColor::Yellow, FString::Printf(TEXT("Some debug message")));
-
TEXT("")
: Asegura que las cadenas sean compatibles con Unreal Engine, en este caso encierra dentro el texto, acompañado de marcadores de formato como %i %f que indican que ahí va un integer o float respectivamente
luego, de cerrar la ") le ponemos una , y le ponemos los inputs, en este caso es la variable number1, que es y debe ser un integer, porque así lo pide el marcador de formato %i -
FString::Printf():
Formatea la cadena con valores dinámicos (como variables), creando la cadena final. -
GEngine->AddOnScreenDebugMessage()
: Imprime el mensaje formateado en la pantalla del juego.
el segundo encerrado con un cuadro verde
Fstring es una clase que maneja y manipula cadenas de texto dentro de UE
FString:Printf es un método de la clase FString, que permite crear una cadena de texto formateada, insertando valores variables en la cadena utilizando un formato similar al de la función printf en C.
Else
Si el if no se cumple, luego de cerrar el } podemos poner un else
En este caso
setea a number1 = 21
pero luego compara number1 == 20
...lo cuál es falso, porque ya lo hemos seteado como 21
entonces no se cumplirá el if, y se irá al else
en este caso, como no estamos formateando textos, no es necesario el FString::Printf
, solamente luego del FColor::Yellow
se pone una coma y se escribe el TEXT("")
else if
es poner un if else tras otro
aquí, después de no cumplirse al primera condición, luego del else, pone un if, para comprobar si se cumple otra condición...y en este caso ahí termina, en el siguiente else ya no pone otro if, pero podría, aunque no es óptimo
Switch
- se escribe switch, luego entre paréntesis la condición, luego {}
- dentro los diferentes casos, se comprobarán/ejecutarán en el órden que fueron escritos, o sea, 42,27,41, no en órden numérico
- luego de cada bloque de ejecución, hay que cerrar con un break, eso corta el hilo de ejecución, sino se hace esto, seguirá ejecutando el siguiente bloque
- al final siempre va un default, el default es el único que no necesita break
switch (condición){
case 1: bloque de ejecución; break;
case 2: bloque de ejecución; break;
case 3: bloque de ejecución; break;
default: bloque de ejecución;
}
En este caso imprimirá
faso
animal1
primero "faso" por la función anterior, resultante del else (no tiene nada que ver con el switch)
luego "animal1" porque se cumplió la función de number1 = 42
si no pusiera un break en el primer bloque de ejecución, lo que imprimiría sería
faso
animal1
animal2
Ejercicios if else
Ejercicio 1
cuando declaras una variable, sí le pones el tipo, y para asignar le pones un =
pero cuando la llamas para el if, solamente escribes el nombre, y le pones == para comparar en el if
Ejercicio 2
Ejercicio 3
Loops
While
mientras
cuando la ejecución llega al while, evalúa la condición, si es verdadero (cualquier valor diferente a 0) se ejecuta otra vez y así hasta que en una de esas evalúe la condición y salga falso
ese cambio debe suceder dentro de las {} del while
si la condición es 0, ya no se ejecuta y sigue el hilo de ejecución
Sintaxis (igual que un if)
while (condition) {
//Este bloque solo se ejecutará si se cumple la condición
}
Do While
Es parecido al while, con la excepción de que la primera vez se ejecuta sin preguntar, luego las demás ya comprueba si la condición se cumple, pero el Do While siempre se ejecuta al menos una vez sí o sí
Sintaxis
do {
//Este bloque se ejecutará sí o sí la primera vez, luego entrará en un loop de comprobar si la condición se cumple para ejecutar el bloque de código
} while (condition);
For
Estructura básica de un for
En BP sería así
int i = 0 es el primer index o index actual
i<10 es el último index
i++ es cuánto va a aumentar entre cada iteración
lo que está en la segunda línea sería el loop body
En este caso, dice que:
(int i = 0; i<10; i++)
int i = 0 indica que el primer index será 0
i<10 que el último será menor a 10
i++ indica que contará de uno en uno
es decir que irá contando de 1 en 1 de 0 a 9, en total 10 iteraciones
loop body: imprimirá "Resultado %f" siendo f la variable resultado, pero aumentando su valor en +1 cada vez (esto indicado por que en la siguiente línea, dice resultado++, es decir que en cada iteración subirá +1
este en cambio, indica que contará desde 50 hasta menos que 500, de 50 en 50→ 50, 100, 150, 200, 250, 300, 350, 400, 450, es decir 9 veces
la variable resultado comenzará en 0 y en cada iteración aumentará 5
es decir imprimirá así:
Resultado: 0
Resultado: 5
Resultado: 10
Resultado: 15
Resultado: 20
Resultado: 25
Resultado: 30
Resultado: 35
Resultado: 40
Break en loops
El break sirve para romper el loop en cuando se cumpla una condición específica, por ejemplo
aquí el index va a ir de 0 a 9 aumentando de 1 en 1
en cada iteración de resultado va a aumentar 1, pero además en cada iteración, va a comprobar que si index es == 5, si es igual a 5, se detendrá la ejecución
Más ejercicios (no resueltos)
Convención de nombres
- UName Las clases derivadas de UObject comienzan por U excepto AActor. por ejemplo sirven para cargar niveles, generación procedural, hacer cálculos
- AName Las clases derivadas de AActor comienzan por A.
- EName Las clases enumerados comienzan por E
- IName Los Interfaces comienzan por l
- FName Los clases estructuradas (Structs, Strings, ) por ejemplo FString, para formatear un texto
- TNome Las plantillas comienzan por T, de templates
- SName Las clases derivadas de SWidget comienzan por S.
Esto nomenclatura se debe respetar: el UHT (unreal header tool) se quejaría en caso contrario
Variable de tipo entero
- int8 para variables de tipo byte signado (1 byte)
- uint8 para variables de tipo byte no signado (1 byte)
- int16 para variables de tipo "short" signado (2 bytes)
- uint16 para variables de tipo "short" no signado (2 bytes)
- int32 para variables de tipo "long" signado (4 bytes)
- uint32 para variables de tipo "long" no signado (4 bytes)
- int64 para variables de tipo "long long" no signado (8 bytes)
- uint64 para variables de tipo "long long" no signado (8 bytes)
los que llevan u, se refiere a unsigned, que no permite los negativos para que el valor sea el doble
Variables de tipo decimal
- floot para variables de tipo con coma flotante (4 bytes)
- double para variables de tipo con coma flotante (8 bytes).
Variables de tipo boolean
- bool para variables booleanos; suelen nombrarse comenzando por "b" minúsculo
Variables de tipo char
- TCHAR para caracteres.
Variables de tipo string
- FName Un tipo textual que se suele usar cuando repetimos mucho el nombre: aguante extensión de una palabra
- FString La cadena de caracteres más básico de Unreal Engine: aguanta extensión de una frase
- FText Un tipo formateable (localización, composición) ya para un texto largo, un párrafo
Conversiones de tipo
acceder a variables y funciones
los morados son funciones
los azules son variables
como en blueprints para hacer referencia una propiedad de un componente, se usa get...GetActorLocation por ejemplo, y siempre con el punto.
por ejemplo
vector.X = GetActorLocation().X;
en este caso todos son absolutos, a menos que lo especifique que sea relativo
también está GetScale3D()
cómo hacer un set:
faltan:
hacer los ejercicios 1-6
preguntar por conversiones sanitize
Reflexión
Consiste en que el propio código sea consciente de sí mismo (nombres de las funciones, variables, etc.)
Permite exponer las clases en el editor. promoverlas para que puedan ser editadas en el editor de unreal
esto en el .h
Class Default Object, es una instancia única de una clase que contiene los valores predeterminados de todos los miembros de esa clase. Es como una plantilla que define cómo deben comportarse y cómo deben estar inicializados los objetos de esa clase cuando se crean en tiempo de ejecución o en el editor.
Constructor: es la primera función del .cpp lanza todo el código cuando compila
BeginPlay y Tick: El BeginPlay se ejecuta 1 vez al inicio del juego y el Tick una vez cada pulso
Están en el .h
y en el .cpp
Diferencia entre función pura e impura
pura: para hacer operaciones matemáticas, no están atadas al flujo de ejecución porque solo devuelven un valor. Estas funciones sí que podrían implementar en h y llamarlas desde el cpp. Si pueden tener inputs de entrada, de algún float, int, bool, etc...pero no necesita estar conectada al flujo de ejecución (hilo blanco)
impura: se hacen en el flujo de ejecución, sí o sí en el cpp, y está atada el flujo de ejecución
pero por lo general siempre se hacen en el cpp
El especificador UCLASS que está en el .h tiene que estar para que pueda ser visible cuando querramos materializar esta clase de c++ en el editor a través de un blueprint
UPROPERTYS
exponer cosas de c++ en el editor!
Primero materializar la clase de c++ en un blueprint...
crear un blueprint, en la clase, buscamos el nombre de la clase que creamos en c++
El Uproperty (para variables) o Ufunctions (para funciones) son especificadores (al igual que el UCLASS) que se tiene que poner encima de todas las variables que quieras tener disponible en el sistema de reflexión (las que quieras disponible en blueprints)
se tiene que poner encima de CADA UNA! no por tener varias debajo va a englobar varias
por ejemplo para exponer 2, tiene que ser así, en cada una
- C++, todo lo de generación procedural, cálculos matemáticos, sistemas, es más óptimo
- Blueprints: Gameplay, mecánicas, dinámicas, es más fácil para implementación
Especificadores de UPROPERTYS
Por ejemplo si le ponemo el especificador BlueprintReadOnly en BP solo podremos acceder a su get, no a set
Si usamos BlueprintReadwrite podemos usar tanto set como get en bp
El VisibleAnywhere lo hace visible en cualquier parte del editor pero no es editable
VisibleInstanceOnly hace que solo sea visible en las instancias o cuando está en el nivel
EditAnywhere este expone el valor en el editor, pero no en el blueprint, por ejemplo aquí las variables IsMoving y Health
EditInstanceOnly
El de Category es para que cuando llames, te aparezcan por categoría, por ejemplo aquí se le puso al Health la categoría de enemigo, entonces salen agrupadas
- Borrar Binaries, Intemediate, DerivedDataCache
- Borrar la solución
- volver a generar la solución con click derecho/Generate Visual Studio project files
UFUNCTIONS
Las funciones de una clase pueden usarse desde C++, pero con las UFUNCTIONS también se pueden usar desde BP. Hay 2 formas de hacerlo
- Llamar desde BP, una función creada en C++
- Implementar en BP, una función creada en C++
Igual que los Uproperties, solo se lleva a BP la primera función que lee debajo del UFunction, OJO que este 👇 ya tiene un especificador "BlueprintCallable" que hace que sea llamable desde BP
UFUNCTION(BlueprintCallable) //El macro "UFUNCTION" hace la función disponible en el sistema de reflexión y el especificador "BlueprintCallable" la hace llamable desde BP
bool IsMovingPeroEsUnaFuncion(); //Esta es una función cualquiera
//EJEMPLO DE ESPECIFICADORES: (SYNTAXIS)
UFUNCTION(specs_1,specs_2,specs_3,...,meta=(metaspec_1,metaspec_2,...))
tipoEjemplo FuncionEjemplo(params_1,params_2,params_3,...);
Especificadores de UFUNCTION
los primeros 4 son los más usados
- BlueprintCallable La función puede ser llamado desde Event Graph de cualquier BP
- BlueprintPure Va a ser pura, se evalúa cuando se activa otra función a la que da el valor
- CallInEditor Genera un botón en el editor (en Details) al ser presionado llama a la función. Puede ser usado para ejecutar una función con valores aleatorios, como para generación procedural, pero! no va a funcionar si devuelven un valor, es decir solo funcionará con funciones void!
- Category igual que en el uproperty, para agrupar por categorías
- DevelopmentOnly La función solo se ejecutará si se ejecuta en modo desarrollo, no es muy usada, ya que al empaquetar el juego para Shipping, por defecto las cosas como los Print, no los considera
Ejemplo
UFUNCTION(BlueprintCallable, Category="Function Category")
void DoSomethingInEditor() {};
Implementable/Native Event (especificadores)
sobrescribir funciones en BP
BluerprintImplementableEvent: sirve para solamente declarar la función en C++ pero implementarla en el event graph de blueprints, por si por alguna razón no se puede o no se sabe hacer en c++ (en blueprint lo crea como un evento, nodo rojo)
UFUNCTION(BlueprintImplementableEvent)
void Funcion_Por_Implementar();
BlueprintNativeEvent
tiene que tener su implementación en C, si gustas puedes sobreescribirla en BP
este especificador es más usual cuando se trabaja en equipo, si es en solitario hay formas más fáciles de hacer esto como con el BlueprintImplementableEvent
UFUNCTION(BlueprintNativeEvent)
void Funcion_Nativa();
- ponerle el identificador en .h (BlueprintNativeEvent)
- cuando la definas en .cpp hay que agregarle al nombre un _Implementation
hay que crearle su estructura en el .cpp, entonces desde el .h ayudándonos del destornillador
Hay que agregarle la palabra "Implementation" a la función, SINO NO FUNCIONA 👇
esto en el .cpp
Punteros y Referencias
Puntero (*
)
- Es una variable que guarda una dirección de memoria.
- Puede apuntar a algo... o a nada (
nullptr
). - Necesita desreferenciarse con
*
para acceder al valor.
int a = 10;
int* p = &a; // p guarda la dirección de a
*p = 20; // cambia el valor de a a 20`
Referencia (&
)
- Es un alias (otro nombre) para una variable existente.
- Siempre debe inicializarse al declararse.
- No puede cambiar de "a quién hace referencia".
- Se usa igual que la variable original.
int a = 10;
int& r = a; // r es otro nombre para a
r = 20; // cambia el valor de a a 20`
Característica | Puntero | Referencia |
---|---|---|
Puede ser nullptr |
✅Sí | ❌No |
Se puede cambiar | ✅Puede apuntar a otra cosa | ❌ Siempre apunta a lo mismo |
Sintaxis | Usa * y & explícitamente | Se usa como una variable común |
Necesita inicialización | ❌ No es obligatorio | ✅ Obligatorio |
Componentes
Solo los actores pueden tener componentes, es una forma de separar la lógica de una entidad en varias entidades
Generalmente se parte de 2 componentes:
- UActorComponent: usado para implementar comportamientos
- USceneComponent: también añade comportamientos y además tiene representación espacial (se puede rotar, escalar, posicionar en el mundo)
Cuando creas un nuevo BP siempre viene con un DefaultSceneRoot
este elemento en C++ se llama "RootComponent" y es de tipo USceneComponent
La función del RootComponent en C++ (o el DefaultSceneRoot en BP) es ser la raíz de todos los demás componentes y organizarlos, sin él, la clase no podría tener representación espacial (PSR) y daría error
el DefaultSceneRoot de BP está pensado como un placeholder para que sea reemplazado con un componente real, por ejemplo un Static mesh 👇
y que quede así
de la misma manera C++, el RootComponent (aunque no aparece en el código inicial de C++) debe ser sobreescrito por un componente real, entonces para crear un componente nuevo hay que hacer lo siguiente
- Declarar un nuevo componente (en .h)
- Instanciarlo (hacerlo real) en (.cpp)
- Ponerlo en la jerarquía en (.cpp)
- Si es el primer que creamos debemos setearlo como Root Component
- Si ya existe un root component, Atacharlo a la jerarquía
Declarar un primer componente (.h)
al igual que cuando creamos un componente en BP, debemos indicar qué tipo de componente es
Si es el primer componente que creamos, debemos setearlo como el RootComponent, por lo tanto sí o sí debe ser tipo USceneComponent (este tipo es como si dijéramos tipo float, bool, int, etc.) y también ponerle el identificador, por ej.
en .h debajo de algún public:
public:
UPROPERTY(EditAnywhere);//Especificador necesario para editarlo en BP
USceneComponent * SceneComponentDePrueba
El *
Es una Referencia
Instanciarlo en la escena (.cpp)
hasta aquí hemos declarado, pero falta instanciarlo (hacerlo real en la escena)
No es un término técnico pero en este caso equivaldría a Instanciar o hacerlo real en el mundo
porque si lo hacemos por ejemplo desde el BeginPlay, se crearía recién cuando ejecutamos el juego y no lo podríamos ver en el Graph editor del BP
no siempre debe ser así, por ejemplo si queremos crear componentes en ejecución, como en el Teleport de XR, pero en este caso sí porque es parte de la estructura de la clase
en .cpp en el constructor
AScore::AScore() //👈el constructor, recuerda que siempre se llama como el nombre de la clase, aquí la clase se llama Score
{
PrimaryActorTick.bCanEverTick = true;
currentScore = 100;
//abajo está lo que importa 👇
SceneComponentDePrueba = CreateDefaultSubobject<USceneComponent>(TEXT("Pepito"));
/* la estructura de esta linea va así:
NombreDelComponente =
CreateDefaultSubobject
<tipoDelComponenteQueSeEstáCreando>
*/
RootComponent = SceneComponentDePrueba;
//Esta linea estamos seteando al compononente que creamos como RootComponent (raíz) en este caso es necesario porque es indispensable que exsta un Root sino daría error (el root por defecto está vacío, por eso no podemos dejarlo así)
}
- Nombre del componente (identificador), "=" setear...
- Función CreateDefaultSubject
- entre <> el tipo de componente que estamos instanciando ,en este caso es un USceneComponent
- (TEXT("...")) en vez del ... poner el nombre que queremos que aparezca en el Blueprint, y no olvidar el ;
- en caso que sea el 1er componente Una linea abajo, setear
RootComponent = SceneComponentDePrueba
, aquí estamos sustituyendo el placeholder por nuestro componente creado
En el editor de UE
- Instanciar clase de C++ como BP (está explicado más arriba)
- Compilar en el IDE (ctrl+B)
- Compilar en el editor y en el BP para que se apliquen los cambios y ahí se verá como hemos creado un componente y lo hemos atachado a la jerarquía
Aparece entre paréntesis () porque en realidad el RootComponent sigue ahí pero está haciendo referencia a otro componente
Declarar un segundo componente (.h)
como no es el primero, ya existe un root component seteado, solo es necesario atacharlo a esa jerarquía
en .h debajo de algún public
public:
UPROPERTY(EditAnywhere);
USceneComponent* SceneComponentDePrueba; //Este es el RootComponent
UPROPERTY(EditAnywhere); //👉NO OLVIDAR para poder editarlo en BP
UStaticMeshComponent* SMComponente//Este es el nuevo Componente, en este caso es un StaticMesh por eso le ponemos el tipo correspondiente
en .cpp en el constructor
AScore::AScore()
{
PrimaryActorTick.bCanEverTick = true;
currentScore = 100;
SceneComponentDePrueba = CreateDefaultSubobject<USceneComponent>(TEXT("Pepito"));
RootComponent = SceneComponentDePrueba;
//nuevo componente 👇
SMComponente = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("SMComponente"));
RootComponent = SceneComponentDePrueba;
S = CreateDefaultSubobject<USceneComponent>(TEXT("Pepito"));
/*aquí la primera linea sigue la misma estructura que el componete anterior:
NombreDelComponente =
CreateDefualtSubobject
<tipoDelComponente> en este caso es un StaticMesh, usamos su tipo correspondiente
(TEXT("NombreDelComponenteEnElBP"));
*/
SMComponente -> SetupAttachment(SceneComponentDePrueba);
/* la segunda linea sí es diferente
NombreDelComponente ->
SetupAttachment
(NombreDelComponente al que se attacha);
}
Rotar Componentes
Es decir acceder a las propiedades de una variable/función y modificarlas
primero hay que pensar dónde (desde qué evento) se ejecutará por ejemplo si queremos que una Mesh rote perpetuamente, podemos ponerlo en el Tick (.cpp)
void APuntaje::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
SMComponente -> AddWorldRotation(FRotator(0.f, 2.f, 0.f));
}
Primero se pone el nombre del Componente
-> la flecha es para acceder a una propiedad/función dentro del Componente
En este caso accede a AddWorldRotation
FRotator es una estructura conformada por Floats
también existen FVector (Vector conformado por floats)
FTransform (Transform conformado por floats)
(0.f, 2.f, 0.f)
los números adentro, son floats
si los escribiéramos como (0.0, 2.0, 0.0) no serían floats sino doubles, que son floats de doble precisión, son más exactos, pero ocupan más memoria, en este caso que estamos haciendo matemáticas 3D, usar floats es suficiente
(0.0f, 2.0f, 0.0f) también se considera como si fueran floats, cualquiera de las 2 maneras vale
Añadir #includes
o dependencias
a veces algunos componentes no aparecen disponibles, necesitan que se importen librerías al principio en los includes para que estén disponibles, esto pasa con un USpotLightComponent por ejemplo
Buscar ese componente en BP
y luego en google buscar por ejemplo USpotLight y buscar su "Include" recordar que los componentes siempre comiezan con U
Esto es de dev.epicgames.com
Entonces habría que copiar y pegar ese include en .h arriba de todo
en .h
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Components/SpotLightComponent.h" //👈Agregar el include necesario
#include "Puntaje.generated.h" //👉 el .generated.h siempre debe ser el último de los includes
Operar Componentes
en .h en public (previo include del componente correspondiente()
USpotLightComponent* LuzParpadeante;
en .cpp, dentro del Tick porque quiero que cambie cada frame
Acceder a las variables de una clase desde otra clase
Primero hay que añadir con un #include
a la clase a la cual queremos hacer referencia, añadir el .h, no el .cpp, sería algo así
#include "MySecondActor.h" //👈aquí lo estamos añadiendo, sin la A
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Components/SpotLightComponent.h"
#include "Puntaje.generated.h"
Luego en el .h hay que crear la referencia a esa clase
UPROPERTY(EditAnywhere) //imprescindible: que haya una variable de este tipo en el nivel
AMySecondActor*secondactor;
//Se pone el nombre de la clase con un A adelante y con un puntero se referencia a un se usa puntero porque estamos tratando con clases personalizadas,componetes o clases propias de Unreal
Es requisito! que en el Actor que estamos referenciando sus variables estén guardadas bajo public:
ahora podríamos acceder a las variables de la clase referenciada, esto en .cpp en el BeginPlay
me quedé en el 1:07 masomenos de la penultima clase
secondactor->variable que queremos
acceder a las variables, ejemplo damage
secondactor->damage
setear una variable
secondactor->damage = 300.f;
pero faltaba inicializar la variable! entonces lo hacemos en el Constructor, como los componentes
secondactor = AMySecondActor::GetClass();
// aquí se está seteando el second actor como...se llama al actor referenciado :: (doble dos puntos) se llama a la función GetClass(); es como setear una variable, ahora cada vez que escribamos secondactor, nos estamos refiriendo a la clase del actor MySecondActor
en el .h del actor principal
UPROPERTY
AMySecondActor