No entiendo, si ya se tiene una variable que marca si tiene una habilidad o no ¿para qué hacer la operación or o and?
Se tiene estas 4 variables:
HABILIDAD_SALTO = 1 # Salto: 0001 en binario
HABILIDAD_CORRER = 2 # Correr: 0010 en binario
HABILIDAD_ATACAR = 4 # Atacar: 0100 en binario
HABILIDAD_PLANEAR = 8 # Planear: 1000 en binario
Para saber si puede saltar basta hacer:
Si HABILIDAD_SALTO == 1
// puede saltar
Estrictamente hablando, no son variables, sino constantes con las cuales representar los grupos de bits que definen las habilidades de un personaje dado. Veamos todo esto de forma más concreta. Imaginemos que tenemos modelado el personaje mediante el tipo siguiente:
class Personaje {
nombre: string;
nivel: int;
habilidades: int;
}
Lo importante en este ejemplo es el campo
habilidades, que nos va a permitir indicar cuáles son las habilidades de un personaje concreto, las cuales serán especificadas mediante las constantes siguientes:
const HABILIDAD_CORRER = 1;
const HABILIDAD_SALTAR = 2;
const HABILIDAD_ATACAR = 4;
const HABILIDAD_PLANEAR = 8;
Luego, en algún punto del programa se crea una instancia concreta de un personaje, digamos, así:
let miPersonaje = new Personaje();
miPersonaje.nombre = "Eren";
miPersonaje.nivel = 1;
miPersonaje.habilidades = HABILIDAD_CORRER | HABILIDAD_SALTAR;
La parte en la que hay que fijarnos es la última línea, que de forma bastante cómoda (y eficiente) nos permite definir todas las habilidades que la instancia
miPersonaje posee, utilizando un único campo
habilidades. Luego en otra parte del programa, podemos agregar un nueva habilidad de la manera siguiente:
miPersonaje.habilidades = miPersonaje.habilidades | HABILIDAD_ATACAR
O si la sintaxis del lenguaje lo permite, así:
miPersonaje.habilidades |= HABILIDAD_ATACAR
Con esto, la prueba siguiente pasa:
if (miPersonaje.habilidades & HABILIDAD_ATACAR) {
// Realizar ataque
}
Desde luego, los cálculos con operadores a nivel de bits se pueden abstraer en un método en el tipo
Personaje:
class Personaje {
// Campos...
tieneHabilidad(habilidad: int): boolean {
return (boolean)this.habilidades & habilidad;
}
}
de manera que la prueba anterior podría quedar así:
if (miPersonaje.tieneHabilidad(HABILIDAD_ATACAR)) {
// Realizar ataque
}
Desde luego, también puedes implementar todo esto mediante booleanos, pero entonces tendrías que crear un campo para cada habilidad en la clase Personaje:
class Personaje {
nombre: string;
nivel: int;
habilidad_correr: boolean;
habilidad_saltar: boolean;
habilidad_atacar: boolean;
habilidad_planear: boolean;
}
Luego, para habilitar habilidades, les asignas valores verdaderos:
miPersonaje.habilidad_correr = true;
miPersonaje.habilidad_saltar = true;
Observa que en este caso las habilidades no tienen nombre fuera de la clase Personaje, de manera que para probar si un personaje tiene o no cierta habilidad tienes que referirte al campo específico que la define:
if (miPersonaje.habilidad_atacar) {
// Realizar ataque
}
Esto no es ningún problema, desde luego. Lo que sí puede ser un problema desde el punto de vista de la arquitectura de software es que agregar una nueva habilidad implica modificar la clase Personaje, lo cual no ocurre con la primera implementación, aquella con máscaras de bits. Por tanto, esta implementación se adhiere más a las buenas prácticas de la arquitectura de software, una de las cuales es programar de tal manera que el código esté abierto a la extensión pero cerrado a la modificación.
Yo, sinceramente, a no ser que tengas que programar ciertas cosas concretas de bajo nivel o que la eficiencia sea un factor esencial para ti (que a juzgar de lo que pones por aquí no es el caso), no los usaría para nada. Para programación "normal" considero que es mejor usar booleanos y las operaciones con booleanos.
Además, la ganancia en eficiencia (por ejemplo de un desplazamiento a nivel de bits vs una multiplicación por 2) no es tan clara. Hoy en día tanto hardware como compiladores están muy optimizados y son muy sofisticados, de manera que lo normal es que a nivel de hardware ya se implemente de la forma más eficiente posible. Y otro efecto de la complejidad de los compiladores es que es difícil predecir si "complicar" un código usando operaciones a nivel de bit realmente va a ser más rápido que otro que no lo use, más allá de medirlo empíricamente. Por otro lado, el uso de operaciones a nivel de bits en estas situaciones tiene el problema de ofuscar el código, de manera que es menos comprensible.
Así pues, en el ejemplo que te dio ChatGPT yo no usaría bits, sino booleanos. Y restringiría el uso de bits a contextos en los que aparezcan de forma natural (protocolos, criptografía, ciertas operaciones con gráficos, etc)
Coincido en que muchas veces el beneficio de utilizar operadores a nivel de bits en términos de eficiencia es insignificante, pero difiero en que estos operadores introduzcan una complejidad real en el código, pues es un concepto que no cuesta mucho aprender y que, una vez comprendidos, permiten realizar implementaciones bastante legibles y más amenas al mantenimiento. En cuanto a la legibilidad, resulta que, por ejemplo, las máscaras de bits permiten reducir el número de argumentos de una función, además de permitir pasar parámetros que pueden ser especificados mediante constantes que dejan claro el objetivo de los mismos. Por ejemplo, en PHP, existe una función para definir qué errores son reportados, la cual recibe un único argumento, una máscara de bits para activar todos los tipos de errores que serán emitidos durante la ejecución del programa:
error_reporting(E_NOTICE | E_WARNING | E_USER_ERROR);
Esto es mucho más cómodo que definir una función (o una variable global booleana) por cada nivel de error, así como una única función con decenas de argumentos, uno para cada nivel de error.
Saludos.