#なぜビット演算が必要なのか?
大きく次の2つのメリットがあります。
・多くのbool変数を1つの変数にまとめる事が出来る
・1つにまとまってるので操作が色々便利
例えば戦闘中のゲームのキャラクターの状態を示す以下のフラグ(bool型変数)があったとします。
bool isSleep; //眠らされているか?
bool isPoison; //毒に侵されているか?
bool isNoMagic; //魔法を封じ込められているか?
bool isConfusion; //混乱して敵味方の区別がつかないか?
bool isAttackDown; //攻撃力がダウンしているか?
bool isDefDown; //防御力がダウンしているか?
bool isAttackUp; //攻撃力がアップしているか?
bool isDefUp; //防御力がアップしているか?
ビット演算を使えば、全て1つの変数だけで制御できます。
ドラクエだとキャラクターの状態変化は数十もあるので1つ1つbool変数にしていたら面倒です。
そして回復魔法でマイナスの状態だけ(攻撃力・防御力アップ以外)を回復させるなど一括操作も簡単にできます。
このページの目的は、最後に載せているゲームのキャラクターの状態を管理するビット演算のコードを理解できるようにする事です。
#だいたいこんな感じ
0001
の眠りと、0010
の毒を足すと、0011
の毒に侵されて眠ってる状態が表現できます。
ただビット演算では、+
や-
ではなく、|
や&
や~
などを使います。
#2進数の書き方
ビット演算するには2進数の書き方を覚えた方が便利です。ついでに16進数の書き方も覚えましょう。
//全て31
var dec = 31; //10進数
var bin = 0B11111; //2進数(0Bか、0bを頭につける)16 + 8 + 4 + 2 + 1 = 31
var hex = 0X1F; //16進数(0Xか、0xを頭につける)16 + 15 = 31
//C#7以降では見やすいように"_"で、区切る事が出来ます
var bin2 = 0B_0011_1111_1001; //2進数
var hex2 = 0X_FF_2F_FF; //16進数
#10進数を2進数で表示
10進数で返されたビット演算の結果は、2進数で確認した方がフラグの状態を理解しやすいです。
Convert.ToStringメソッドの第2引数に2を指定すると、第1引数を2進数に変換されます。
ちなみに16だと16進数に変換されます。
PadLeftメソッドは第1引数の桁になるまで、第2引数で文字列の左側を埋めます。
using System;
public class Prog {
public static void Main() {
int[] array = { 1, 2, 3, 4, 5, 0B111, 0XFF };
foreach( int i in array) {
Console.WriteLine( Convert.ToString(i, 2).PadLeft(8, '0') + " " + i);
}
}
}
//00000001 1
//00000010 2
//00000011 3
//00000100 4
//00000101 5
//00000111 7
//11111111 255
#ビット演算子
2つの数値をビット演算子
を使って計算します。1+1=2の+
みたいものです。
特に良く使う |
と &
はどっちがどっちか分からなくなるので、自分は次のイメージで覚えています。
#####|
は、片方だけでも1があれば、回転して残像で両方1になる。
#####&
は、両方の穴に1が収まって、はじめて1になる。
using System;
class Prog {
static void Main(string[] args) {
int a = 0B_0101;
int b = 0B_0110;
Print( a&b ); //両方1なら1 論理積
Print( a|b ); //どちらか1なら1 論理和
Print( a^b ); //片方だけ1なら1 排他的論理和
Print( ~a ); //0なら1に、1なら0に 反転
Console.WriteLine("");
Print( a<<1 ); //左にシフト
Print( a>>1 ); //右にシフト
}
static void Print(int i) {
Console.WriteLine( Convert.ToString(i, 2).PadLeft(4, '0'));
}
}
//0100
//0111
//0011
//11111111111111111111111111111010
//1010
//0010
#良く使う演算子のパターン
ビット演算はかなり特殊なので、最初は分からなくても実際に使う段階で理解出来ます。こういうものかと深く考えずに暗記するのが早いです。
左からn番目にフラグを立てる i | (1 << n)
int b = 1 << 2; //0100 10進数だと4を表します
int i = 0B_0001;
i |= (1 << 2); //0101 10進数だと5を表します
同じ値をXOR 演算子 ^
を使ってゼロクリアする。
int i = 0B_0101;
i ^= i; //0000
左からn番目のフラグを消す i &= ~(1 << n)
int i = 0B_1111;
i &= ~(1 << 2); //1011
左からn番目のフラグが立っているか確認する i & (1 << n)
int i = 0B_0101;
Convert.ToBoolean( i & (1 << 2)); //True
int i = 0B_0101;
bool b = (i & (1 << 2)) != 1; //True
複数のフラグをまとめて、マスクビットを作る
int a = (1 << 1); //0010
int b = (1 << 2); //0100
var mask = a |= b; //0110
int c = 0B_1111; //1111
c &= ~mask; //1001
#enumを使ってビット演算を見やすく
ビット演算は便利ですが、フラグを数値だけで管理すると分からなくなりそうです。(0B_0100は「毒」で、0B_1000は「混乱」など自分で決めても間違えそうです‥)
enumの前に、FlagsAttribute属性を指定することで、列挙型の名前でビット演算のフラグを管理出来ます。(Flagsを外すと数値に対してビット演算しません[Flags]
をコメントにすると違いが分かります)
using System;
class Prog{
[Flags] //enumをビットフラグにするFlagsAttribute属性
enum Col:short {
Red = 1 << 0, //0001
Green = 1 << 1, //0010
Blue = 1 << 2, //0100
};
static void Main() {
for(int i = 0; i <= 8; i++)
Console.WriteLine("{0} {1} {2}",
i,
Convert.ToString(i, 2).PadLeft(4, '0'),
(Col)i);
}
}
//0 0000 0
//1 0001 Red
//2 0010 Green
//3 0011 Red, Green
//4 0100 Blue
//5 0101 Red, Blue
//6 0110 Green, Blue
//7 0111 Red, Green, Blue
//8 1000 8
ちなみに自然界では、赤と緑をかけ合わせると黄色になり、赤と青をかけ合わせると紫色になり、赤、緑、青、全てをかけ合わせると無色になります。三原色をビット演算で組み合わせる事で虹色も表現する事ができます。
1つ具体例として、Taskのオブションで使うフラグの例を見てみましょう。
TaskCreationOptions.LongRunning | TaskCreationOptions.PreferFairness
は、列挙型のフラグです。
var task = new Task( () => Method_A(),
TaskCreationOptions.LongRunning | TaskCreationOptions.PreferFairness);
task.Start();
列挙型の説明を見ると、フィールド毎に番号がふってありますが、全て2の倍数になっています。
この整数値をビット演算で組み合わせて、1つの引数で複数のフラグを立てる事を可能にしています。番号だけだと分かりにくいので列挙型にして名前を付けている訳です。
#ゲームのキャラクターの戦闘状態を制御
ビット演算の締めくくりに、ゲームの戦闘中のキャラクターの状態を次々と変化させていきましょう。
using System;
[Flags]
public enum Status { //10進数で書く場合は、2の倍数にする
Normal = 0,
//マイナスの状態異常
Sleep = 1, //眠っている 0001
Poison = 2, //毒に侵されている 0010
//プラスの状態変化
AttackUp = 4, //魔法で攻撃力アップ 0100
DefUp = 8, //魔法で防御力アップ 1000
}
public class Prog {
public static void Main() {
var chara = Status.Normal; //キャラクター
chara |= Status.AttackUp; //味方の魔法で、攻撃力アップ!
Print( chara); //AttackUp
chara |= Status.Poison; //敵の毒に侵される!
Print( chara); //Poison, AttackUp
chara |= Status.Sleep; //敵に眠らされる!
Print( chara); //Sleep, Poison, AttackUp
//回復魔法(眠りと毒だけを解除、攻撃力・防御力アップはそのまま)
Status heal = Status.Sleep | Status.Poison;
chara &= ~heal; //味方の回復魔法で復活!
Print( chara); //AttackUp
//攻撃力・防御力どちらか、もしくは両方がアップしているか確認
Status up = Status.AttackUp | Status.DefUp;
bool b = (chara & up) != Status.Normal; //True
chara ^= chara; //全ての効果を無効にする魔法を唱える!
Print( chara); //Normal
}
//状態を確認するメソッド
static void Print( Status s){
Console.WriteLine( "{0} {1}", Convert.ToString( (int)s, 2).PadLeft(4,'0'), s);
}
}