Au début de l'ère de l'informatique, binaire et hexadécimal étaient un mode de vie, probablement parce que les langages de haut niveau (comme BASIC) étaient simplement trop lents. Ces jours-ci, avec la puissance du PC le plus moyen, vous n'avez plus besoin de savoir quoi que ce soit et vous pouvez faire beaucoup de choses, car la vitesse de la machine et sa construction CPU plus complexe compenseront tout vient cette approche a.
Comme un exemple très simple, dans le passé, multiplier par 32 aurait pu prendre plusieurs cycles CPU à exécuter, alors qu'une simple opération binaire pour faire la même chose aurait pris seulement 1. Comme les machines sont devenues plus complexes, elles ont aussi réduit le temps de nombreuses instructions complexes sont exécutées de telle sorte que, maintenant, une multiplication de 32x32 bits peut bien prendre seulement un cycle - même chose que l'opérateur binaire. C'est une bonne nouvelle, bien sûr, car cela signifie que vous n'avez plus besoin d'optimiser chaque ligne de code que vous écrivez, mais si c'est le cas, devriez-vous vraiment vous préoccuper de binaire?
La réponse est définitivement "oui, vous devriez". Tandis que c'est vrai, vous pouvez toujours obtenir des accélérations - et parfois elles peuvent être significatives - en utilisant binaire et hexadécimal pour mieux connaître le CPU et aussi améliorer l'écriture de code, être capable de mieux emballer les données et de faire certaines tâches beaucoup plus simple. Cette page va expliquer un peu ce qu'est le binaire ainsi que comment il peut être utilisé lors de vos jeux.
Regardons d'abord la théorie binaire la plus fondamentale - comment les nombres sont créés. Jetez un oeil à cette table:000 = 0
001 = 1
010 = 2
100 = 4
Chaque 1 ou 0 représente un seul bit de données, et comme vous pouvez le voir, cela signifie qu'en binaire, 10 équivaut à 2! Chaque bit est 2 fois la valeur précédente avec le premier bit étant égal à 1. Donc bit 2 = 2, bit 3 = 4, bit 4 = 8 et ainsi de suite (comme indiqué ci-dessous dans ce tableau octet - un octet est une collection de 8 bits):00000001 = 1
00000010 = 2
00000100 = 4
00001000 = 8
00010000 = 16
00100000 = 32
01000000 = 64
10000000 = 128
C'est bien si vous voulez des nombres qui sont une puissance de 2, mais comment créons-nous des nombres plus complexes? Eh bien, un seul nombre binaire ne peut stocker qu'un 0 ou un 1, et c'est tout, alors pour les nombres plus complexes, nous devons ajouter des bits ensemble. Si par exemple nous voulions faire 6, nous ajouterions 4 et 2 ensemble comme ça.00000010 = 2
00000100 = 4
00000110 = 6
Ceci est vrai pour tous les nombres binaires, et comment l'ordinateur compose un nombre interne. Prenons un exemple un peu plus compliqué: 23. Le nombre 23 est composé de 1 + 2 + 4 + 16 ou 00010111. Que diriez-vous d'un exemple beaucoup plus complexe: 196? Eh bien, c'est fait à partir de 128 + 64 + 4 ou 11000100. Donc en fait pas vraiment complexe. Si nous commençons à faire des valeurs en dehors de la plage d'un octet (qui peut stocker des nombres de 0 à 255), il commence à être un peu plus difficile à suivre. Par exemple, 217 361 est 110101000100010001 en binaire. Ou, 1 + 16 + 256 + etc... Les règles sont les mêmes quelle que soit la valeur exprimée - chaque nombre est créé en ajoutant plusieurs bits ensemble.
Qu'est-ce que cela signifie en binaire? Eh bien, disons que vous voulez stocker un true ou false comme une valeur. Habituellement, les compilateurs utiliseront un INT (un INT est généralement défini comme un nombre 32 bits signé), puis l'assignent simplement à 0 ou 1. Cependant, ayant seulement 2 états, un true / false la valeur est idéale pour stocker dans un peu, et si nous avons fait cela, nous pourrions stocker 32 true / false bits pour chaque INT plutôt qu'un seul.
Comment ferions-nous cela? Eh bien assez facilement, il s'avère:flags = flags | 1;
Le "|" L'opérateur est un OR bit à bit, ce qui signifie que l'instruction ORs 1 ci-dessus est en drapeau. Si vous vous souvenez de plus tôt, l'utilisation d'un 1 définira le premier bit. Si nous voulions définir le second bit, nous ferions ceci:flags = flags | 2;
Nous OU dans 2, parce que le modèle de bits 00000010 est égal à 2. Alors, que fait exactement l'opérateur OU binaire? Eh bien, il fusionne tous les bits en une seule valeur, comme ceci:010110100
110011001
110111101
Voici ce qu'on appelle une table de vérité pour l'opérateur OR:00 | 00 = 00
00 | 01 = 01
01 | 01 = 01
01 | 00 = 01
Donc, là où il y a une valeur avec 2 zéros, ça restera nul. L'avantage d'utiliser BITS comme état vrai / faux, c'est qu'ils vous permettent de définir plusieurs indicateurs en une seule opération, ce que vous ne pouvez pas faire avec une valeur booléenne normale. Par exemple, disons que le bit 1 est un indicateur "actif" et le bit 3 est un indicateur "visible". Nous pourrions définir les deux en faisant ceci:flags = flags | 5;
C'est parce que 5 est 00000101 en binaire, et suivant la règle ci-dessus, la variable "flags" obtiendra ces deux bits fusionnés avec les siens. Donc, même si le bit 1 était déjà défini, l'opération fonctionne toujours et le bit 3 sera également défini.
Qu'en est-il de la compensation des drapeaux? Eh bien, c'est là que l'opération binaire ET entre en jeu. Lorsque vous ET quelque chose vous les bits qui sont définis dans le masque sont conservés, tandis que les bits qui sont claires dans le masque, sont supprimés - comme ceci:01110010101
00110000100
00110000100
Comme vous pouvez le voir, où il y a un bit dans chaque valeur, le bit est conservé, et où il y a un mélange ou 0 et 1, ceux-ci sont réinitialisés à 0. Voici la table de vérité pour AND:00 & 00 = 00
01 & 00 = 00
00 & 01 = 00
01 & 01 = 01
Donc, seulement quand il y a un peu à chaque endroit, il sera conservé. Cela signifie que, tout comme vous pouvez définir plusieurs indicateurs à la fois, vous pouvez également effacer plusieurs indicateurs à la fois. Par exemple, prenons le cas ci-dessus, mais cette fois les effacer. Nous voulons effacer les bits 1 et 3 (en nous donnant la valeur 5), mais en rappelant le tableau de vérité ci-dessus, nous voulons garder tous les autres bits, et effacer les bits 1 et 3. Ce serait un masque binaire "de 11111111111111111111111111111010 (32bits). Ce masque conserve tous les bits actuellement définis, mais efface les deux bits que nous voulons réellement effacer. Donc si j'avais une valeur de 1000111011 et que je voulais effacer les bits 1 et 3 en utilisant le masque ci-dessus, je finirais par ça...00000000000000000000001000111011
11111111111111111111111111111010
00000000000000000000001000111010
C'est très bien, mais si nous devions régler ce problème chaque fois que nous devions enlever des drapeaux, cela deviendrait fastidieux. Ce dont nous avons besoin, c'est d'un moyen de retourner les bits facilement (et de préférence sans coût CPU). Heureusement, il existe un moyen facile de le faire en utilisant l'opérateur NOT.
L'opérateur NOT est juste ce qu'il dit - PAS ces bits. Voici une table de vérité pour NOT.~00 = 11
~01 = 10
~10 = 01
~11 = 00
Cet opérateur rend la suppression des drapeaux très simple, et mieux encore, c'est généralement une optimisation du temps de compilation si vous utilisez un nombre constant (c'est-à-dire pas une variable) alors le compilateur retournera les bits automatiquement pour vous. Prenez cette déclaration où nous voulons effacer les bits 1 et 3 à nouveau:a = a & ~5;
Cela se résumera à "a & 11111111111111111111111111111010". Cela rend la vie assez simple en termes de compensation des drapeaux.
Le dernier opérateur que nous voulons regarder est EOR (OU Exclusif, parfois appelé XOR), cet opérateur retourne les bits définis dans les deux valeurs. Voici la table de vérité EOR:0 ^ 0 = 0
0 ^ 1 = 1
1 ^ 0 = 1
1 ^ 1 = 0
C'est un curieux, mais incroyablement utile. Par exemple, disons que nous voulons un compteur qui compte simplement de 0 à 1 et revient à 0 (basculant entre 0 et 1), nous pourrions en ajouter un et faire un IF pour voir s'il est à 2, puis le réinitialiser à 1. Ou... nous pourrions ajouter 1 et ensuite ET avec 1 (depuis 01 + 01 = 10, et 10 & 01 = 0) ou nous pouvons le faire:a = a ^ 1;
Ce que cela fait, c'est la première fois à travers est 0 ^ 1 = 1, puis la deuxième fois 1 ^ 1 = 0, basculant ainsi les choses de 0 à 1.
Donc - OR (|), AND (&), NOT (~) et EOR (^) permettent de manipuler les bits avec une relative facilité, nous permettant, au niveau le plus simple, de contrôler plusieurs bits à la fois. Nous pouvons évidemment utiliser ces opérations pour d'autres choses lors du développement de nos jeux, comme masquer des sprites, effectuer des opérations MOD entières (en utilisant AND) ou faire de jolis compteurs en boucle.
Comment un ordinateur ajoute-t-il? Eh bien, regardons un exemple très simple 1 + 1.00000001
00000001
00000010
Tout comme les additions normales, nous ajoutons des nombres ensemble et nous débordons dans la colonne suivante, mais contrairement à une addition décimale normale, nous pouvons seulement aller à 1, pas 9. Donc, ajouter 1 + 1 signifie que nous débordons dans 10. Alors regardons à un exemple plus complexe.01011011 = 91
00101101 = 45
10001000 = 136
Il est évidemment plus difficile à voir ici, mais les débordements se succèdent jusqu'à ce qu'il n'y en ait plus dans une colonne - ou 2 à quel point un débordement fait 3 et il reste là. Heureusement, vous n'avez jamais à vous inquiéter à ce sujet à moins que vous vouliez ajouter de très grands nombres ensemble (comme les nombres de 2x128bit). Il convient également de noter que les ordinateurs peuvent seulement ajouter (ou soustraire, multiplier ou diviser) 2 nombres à la fois, même SIMD est basé sur 2 calculs à la fois, mais en effectuant plusieurs calculs en parallèle. Prenez 19 + 19 + 19. Être humain, nous pouvons ajouter tous les 9 ensemble, porter le 2 et puis nous allons! Mais les ordinateurs ne peuvent pas faire cela - ce qu'ils peuvent faire est ceci: (19 + 19) + 19. Ils feront donc chaque calcul par blocs de 2.
Les calculs binaires qui nous intéressent et qui sont d'une grande utilité sont la multiplication et la division. Les ordinateurs ne se multiplient que par 2, et pour en faire plus, les nombres sont séparés, puis tous les résultats sont additionnés. Prenons quelques exemples très simples en premier. 4 * 2 = 8. Maintenant, pour multiplier par 2 en binaire, nous décalons tous les bits vers la gauche de un. Comme ça:00000100 * 2 = 00001000 = 8
Tous les bits dans ce cas ont été déplacés vers la gauche par un, le faisant passer du 3ème bit au 4ème, et en changeant la valeur de 4 à 8. Que diriez-vous d'un plus grand nombre?101 = 01100101 * 2 = 11001010 = 202
Encore une fois, tous les bits se déplacent sur un, et ce multiple de 2. Alors, que diriez-vous d'un multiple de 4? Facile, nous décalons tout laissé par 2, plutôt qu'un. Alors, qu'en est-il de 16 ou 128? Cela nécessiterait un décalage vers la gauche de 4 bits ou 7 bits respectivement. Ceci est incroyablement utile. cela signifie que nous pouvons faire de simples multiplications en déplaçant simplement des bits. Pour ce faire, nous utilisons l'opérateur SHIFT <<. Voici quelques exemples:00000001 << 1 = 000000010 = 2
00000001 << 2 = 000000100 = 4
00000001 << 3 = 000001000 = 8
00000001 << 4 = 000010000 = 16
00000001 << 5 = 000100000 = 32
00000001 << 6 = 001000000 = 64
00000001 << 7 = 010000000 = 128
00000001 << 8 = 100000000 = 256
Maintenant, en plus d'être très utile pour les multiplications rapides / simples, il est également très utile pour définir des BIT spécifiques, sans avoir à comprendre la valeur du bit. Disons que nous voulions définir le bit 27, quel est ce nombre? (67108864 en passant!), Bien nous pouvons utiliser la syntaxe ci-dessus pour définir facilement des drapeaux comme ceci:A = A | (1<<27)
Ok... en fait, ce serait le bit 26 de la façon dont j'ai décrit les choses jusqu'à présent (comme les bits ont commencé à un), mais en fait... les bits commencent au bit 0, et montent, pas au bit 1 Donc, bien qu'il y ait 32 bits dans un INTEGER, les bits vont de 0 à 31, pas de 1 à 32. C'est en fait très utile, car nous pouvons maintenant configurer CONSTANTS pour les nombres de bits.
Supposons que le bit 27 soit un drapeau actif et que le bit 0 soit un drapeau explose. Comment pouvons-nous définir les deux?ACTIVE= 27;
BOOM = 0;
A = A | (1<<ACTIVE) | (1<<BOOM);
Cela peut ressembler à beaucoup de code, mais si ces nombres sont des constantes, le compilateur pré-compilera ces opérations dans une seule valeur afin que nous finissions avec ceci comme code réel.A = A | 13421772;
Effacer ces bits (comme nous l'avons vu ci-dessus) consiste simplement à utiliser le modificateur NOT, comme ceci:A = A & ~((1<<ACTIVE) | (1<<BOOM));
Cela nous permet donc de définir et d'effacer tous les bits souhaités et nous permet également de compresser massivement les structures de données. La compression des structures de données est une bonne chose, car si vous utilisez moins de mémoire, vous obtenez moins d'erreurs de mémoire cache et votre code s'exécute plus rapidement. En d'autres termes, quoi de plus rapide, de copier 32 Mo ou de données, ou de 4 Mo? Eh bien, très clairement 4 est. Donc, si vous pouvez regrouper tous vos drapeaux dans un seul accès à la mémoire, c'est bon!
Jetons un coup d'oeil à la façon dont vous faites la division, et pourquoi cela va être si utile. Prenons un nombre simple - 64 - et divisons par 32:64 / 32 = 01000000 >> 5 = 00000010
Donc, il vous passez le seul bit par 5 ( ce qui est le nombre de quarts de travail requis pour 32 - regardez ci - dessus), ce qui nous donne 2. Mais ce qui se passe si ici sont autres bits là - dedans? Eh bien, jetons un coup d'oeil:68 / 32 = 01000100 >> 5 = 00000010
Alors voilà. C'est exactement pareil. Les bits que nous décalons sont simplement perdus. C'est en fait très utile, car quand on a besoin du reste, il y a un moyen encore plus facile de l'obtenir, ce que nous verrons dans un instant. Mais d'abord, prenons un exemple pratique. J'ai une position X et Y, et je veux obtenir la cellule de la grille dans laquelle elle tombe, où la grille a une taille de 32x32. Cette méthode permet de stocker des objets, des collisions, des drapeaux - toutes sortes de choses, et d'y accéder très rapidement. Alors on y va:var X_index = x>>5;
var Y_index = y>>5;
cell_data = mygrid[# X_index,Y_index];
C'est rapide, très rapide. Cela évite la nécessité de faire une division en virgule flottante, puis un calcul floor () - qui s'additionne.
Alors, et si nous voulions le reste? Peut-être que ce reste est utilisé comme une sorte d'ordre ou quelque chose, quelle que soit la raison, obtenir un reste est aussi simple que de faire un ET:var r = x & 31
var X_Index = x>>5;
Maintenant, les plus perspicaces parmi vous ont peut-être remarqué que nous avons utilisé les deux ici (comme c'est souvent le cas), mais ce n'est que quelques instructions. Mais pourquoi le 31? Eh bien, comme le bit 5 est 32, alors tous les bits ci-dessous seraient 31, et c'est le reste maximum donc c'est ce que nous ET avec (nous pourrions aussi utiliser ((1 << 5) -1) ce qui ferait 32-1 = 31. Maintenant, si je devais faire ceci sans comprendre binaire, cela ressemblerait à ceci:var r = x mod 32;
var X_Index = floor(x/32);
Alors pourquoi est-ce bien pire? Eh bien, afin de diviser par 32, nous devons exécuter une division en virgule flottante - ce qui prend évidemment du temps, mais pour faire le mod 32, vous devez en faire un autre! Si nous faisions cela en assembleur, nous obtenons en réalité les deux valeurs dans une division, mais vous ne l'obtenez pas dans les langages de haut niveau (enfin... pas très souvent), et vous devez donc faire tout le travail deux fois. Cela ajoute, surtout si vous faites une boucle serrée avec beaucoup de calculs comme celui-ci. Les divisions entières comme indiqué ci-dessus aident vraiment à optimiser votre jeu.
Puisqu'il peut s'agir d'un concept assez complexe à saisir puis à appliquer à des situations de programmation réelles, vous trouverez ci-dessous une série de petits exemples pouvant être appliqués à n'importe quel jeu créé avec GameMaker Studio 2.
GameMaker Studio 2 utilisent souvent la fonction place_free(), puis, lorsqu'une collision est détectée, essayez de sortir lentement l'objet soit en bouclant une position x ou y (ou quelque chose) tout en continuant à exécuter cette fonction, soit en utilisant le move_outside_all() fonction.
Alors, quel est le moyen le plus rapide de le faire? Eh bien, si nous utilisons des carreaux de power-of-2 appropriés, nous avons une méthode très simple et rapide. Si nous nous déplaçons correctement, et que nous sommes passés dans un bloc de collision, alors nous savons que tout est aligné sur 32, nous devons donc également aligner l'image-objet sur une limite de 32 pixels - de préférence celle de gauche - est sorti de la collision. C'est vraiment facile, connaissant les règles que nous avons utilisées plus haut pour obtenir le reste, et sachant comment obtenir l'inverse des bits, nous pouvons simplement faire ceci:X = x&~31;
C'est vrai, c'est tout ce qu'il faut pour aligner à une limite de 32 pixels. En changeant le 31 nous pouvons aligner à tout ce que nous aimons - tant que c'est une puissance de 2. (Ceci est l'équivalent de diviser par 32, puis en multipliant par 32, supprimant ainsi les bits inférieurs.)
Si nous voulions aligner à droite, alors nous ferions ce qui précède, mais ensuite ajouter 32 pour le déplacer dans la prochaine tuile. Simple. Tout cela rend l'ensemble du code de collision plus rapide et vous permet de passer le temps CPU là où vous en avez vraiment besoin.
Disons que vous avez un niveau avec quelques portes, et une clé pour chacun. Comment pouvez-vous facilement marquer une clé pour une clé? Normalement, vous devez simplement attribuer une identification à la clé et à la porte. Alors que faire si vous vouliez une clé pour ouvrir 2 ou 3 portes? Facile. Vous utilisez un MASQUE. La porte aurait un seul "bit" assigné comme si door_id = 1 (0001), un autre avec door_id = 2 (0010), door_id = 4 (0100), door_id = 8 (1000) et ainsi de suite. Si nous voulions que la clé ouvre la porte 1 et 3, alors la clé aurait le MASQUE de 5 (qui est 101 en binaire). Si nous effectuons un ET de cela, et il sort "non nul", alors nous savons si la clé peut ouvrir la porte. Vous pouvez également avoir des clés qui n'ont rien ouvert en ayant un MASQUE de 0. Voir le code ci-dessous pour la vérification réelle:if( (key_id & door_id) !=0 ) { opendoor(); }
Disons que nous voulons un simple compteur d'animation, comptant de 0 à 15 (comme nous avons 16 images d'animation), maintenant nous pouvons soit faire un incrément, puis faire un IF, ou nous pouvons utiliser notre connaissance de binaire et supprimer le SI complètement. Les IF sont lents, et si nous n'en avons pas besoin, nous devrions les supprimer.counter = (counter+1)&31;
Puisque 16 frames est une puissance de 2 chiffres, et que 0 est inclus dans le compteur, nous pouvons réduire le nombre POW2 de 1 et l'utiliser comme un MASK, et en utilisant cela nous pouvons l'utiliser pour envelopper notre compteur. Si le compteur passe de 15 à 16, nous obtenons le modèle de bits 10000, et si nous avons ensuite 15 avec le modèle de bits 01111, nous obtenons 0. Cela signifie que le code ci-dessus est très utile pour l'encapsulation des compteurs - tant que vous utilisez les numéros d'images POW2.
Et si vous vouliez vérifier si quelque chose était une puissance de 2? Eh bien, voici une petite astuce.. Cela retournera VRAI si argument0 est une puissance de 2.return (argument0&(argument0-1))==0;
Donc, si nous avions le numéro 51 (110011), qu'est-ce que cela fait? Eh bien, nous obtenons ce... 110011 & 110010, ce qui nous laisse évidemment avec FALSE, car il y a beaucoup de "bits" à gauche après l'ET. Si nous avions 64 1000000, alors cela devient ce... 1000000 & 0111111 qui nous laisse 0, donc c'est TRUE.
Voici un petit morceau de code à aligner à la puissance de 2 chiffres. (1,2,4,8,16 et ainsi de suite). Cela peut être très utile pour l'allocation de mémoire ou pour s'assurer que vous écrivez des données aux limites appropriées.Dans cet exemple, argument0 doit être aligné sur argument1 octets, où argument1 est une puissance de 2 nombre. Ce petit script arrondit à la limite suivante du nombre désiré.return (argument0 + (argument1-1)) & ~(argument1-1);