ビット(ビット演算子)の使い時について。
プログラムの中で「なるほど、こういう使い方をするんだ〜」と知る機会があったので自分の整理がてら共有。
flagとかならまだしも、最初はなかなか手段の候補に上がりにくいものと思うので、、
本記事対象者:
ビットやAND、ORが何かは知っているけど、普段コード書く中で使ったことがない人
二進数とか高校の時とかにうっすら触った記憶のある初心者プログラマー
#最初に
ビットといっても数字を見ただけで何を表すのかなかなかピンとこないので、シートを置いておきます。
プログラムでの表記の仕方について、
2進数は頭に0bを付ける(例:2→0b2)
8進数は頭に0を付ける(例:2→02)
16進数は頭に0xを付ける(例→0x2)
普段何も考えずに使っているのが10進数。
#ビットを使うと何が楽になるのか
複合条件を、すべて一つの変数で表すことができます。
例えばこういうものがあるとします。
状態異常名 |
---|
どく |
まひ |
やけど |
ねむり |
こおり |
実在のゲームとは違って、これらの症状を複数の値を保持できて
値を付け足したり増やしたりさせるとすれば、どのように処理を書きますか?
$poison = null;
$paralysis = null;
$burn = null;
$sleep = null;
$ice = null;
/* 略 */
private function poisonAttack($poison)
{
$status['poison'] = 1;
return $poison;
}
あるいは配列にしてみたり。
$status = [
'poison' => null;
'paralysis' => null;
'burn' => null;
'sleep' => null;
'ice' => null;
];
/* 略 */
private function poisonAttack($status)
{
$status['poison'] = 1;
return $status;
}
なんて、私だったらしていたかもしれまん。
配列にしてしまえば複数必要な時も、引数で渡し易くなるのかもしれませんが、
さらにこれらの状態全てをdbに登録しろなんて言われたら、扱うカラムの数が増えてしまいます。
それしか方法が無ければやるっきゃないですけど、
もし配列ではなく、status一つでどくまひやけどねむりこおりせんとうふのうの全てを表せたらそうしたくないですか?
それを可能にするのがビットです。
使い方
やり方を詳しく見ていきます。
##1.二進数を使って複数の状態を表す
症状名 | 2進数 | 名 |
---|---|---|
どく | 000001 | poison |
まひ | 000010 | paralysis |
やけど | 000100 | burn |
ねむり | 001000 | sleep |
こおり | 010000 | ice |
せんとうふのう | 100000 | death |
二進数の欄を見てもらえば分かる通り、これが複数の状態を保持できる仕組みです。
桁数が全部で6桁あって、左から順番に
「せんとうふのう」「こおり」「ねむり」「やけど」「まひ」「どく」
を表すようにしています。
そして中身が0だったら状態にかかっておらず、1だったらかかっています。
なので例えばプレイヤーが「どく」「まひ」「やけど」状態だったとしたら、
000111と表し、それをstatusに持たせます。
そこからさらにプレイヤーがこおり状態になったとしたら、statusの中身は
010111、なんでもなおしが使われたら000000、HPが0になったら100000と表してあげます。
プログラムに書きます。
(これは余談ですが、最初私は一つのクラスの中での処理を想定して変数の中に症状の値をセットしていましたが、ネットでビット演算子の使い方を見てみると、みんなそれ用のクラス作ってconstで定義していたのでそれに習います。ビット演算子がいつもconstで定義されているかは仕様によるかもですが、中身が変わらないものはconstで定義したほうがいいから、今回もそれでいきます。)
class Status {
//二進数を表す時は、頭に「0b」をつける。でないと10進数扱いになってしまう。
const POISON = 0b000001;
const PARALYSIS = 0b000010;
const BURN = 0b000100;
const SLEEP = 0b001000;
const ICE = 0b010000;
const DEATH = 0b100000;
}
##2.状態を追加する操作を書く
次に状態を操作する処理を実際にプログラムで表してみます。
内容は以下のようになります。
- 正常な状態から、どく状態になる
- どく状態からさらにまひ状態も追加される
- どくけしでどく状態のみが回復する
初心者(少なくとも私)にとっては、普段if文などで使っている演算子と似たようなものが、条件分岐ではなくビットの値を操作するのに使われているのがネックでした。
今回やりたいことは、状態異常を追加すること。
基本的に、もともとある状態を保持しつつ別の要素を追加させる時は「ビット和(|)」を使えばだいじょうぶです。(理屈はあとで)
まずは「1.正常な状態から、どく状態になる」を書くとこうなります。
$status = 0b000000;
/* 略 */
private function attack($status)
{
$status = $status | Status::POISON; //0b000001;
return $status
}
//$status = 0b000001;
$status | status::POISON
の中身はこうです。
000000
+)000001
000001
ここで使っている論理和というものは、どちらかに1が含まれていたら1を返すようになっています。
つまり、statusとPOISONの同じ桁を比較し、どちらも0の場合は0にし、どちらかに1が含まれていたら1を返すようになっています。
なので前者の状態を維持したまま、後者の状態を付け加えることができます。
同じように「2.どく状態からさらにまひ状態も追加される」をやってみます。
//先ほどの
//$status = 0b000001
private function attack($status)
{
//$attackにparalysisが渡されたとして
$status = $status | Status::PARALYSIS; //0b000020;
return $status
}
//$status = 0b000011
000001
+)000010
000011
「3. どくけしでどく状態のみが回復する」については、状態異常のステータスが1だったものを0にしたいので、論理演算子の排他論理和(^)を使います。
状態異常のステータスが**1の時だけ1を0にしたいので否定(~)**を使います。※12/18追記参照
class Tool {
const HEAL_POISON = 0b000001;
}
//先ほどの
//$status = 0b000011
private function useHealPoison($status)
{
$status = $status & ~(Tool::HEAL_POISON); //「~」の中身が否定になります
return $status
}
//$status = 0b000010
排他論理和は前者と後者を比べて、どちらか一方のみにセットされている時にセットされます。
...つまり前と後の同じ桁の部分のビットを比較して、異なる数字の時にのみ1を返します。
否定(~)で0b000001の0と1を逆にセットし、ビット積をすることによって、どく以外の状態異常を残しつつ、毒だけ消すようにしています。
~(0b0000001) は 0b1111110 になる
0000011
&)1111110
0000010
#まとめ
複合した条件を処理する時は、ビット演算子を手段の選択肢に入れてみよう!
言いたいことは、それだけでした。
#参考
「PHP: ビットマスク(ビット演算)で複数フラグ管理 | をぶろぐ」
私も理解がまだまだ浅くて...とても参考にさせていただきました。
(整理しようと思いつつ、例とか全然思いつかなくて...)
#※12/18追記
最初は排他論理和で説明していたのですが、それだと「どく状態じゃない時にどくけし使われたら、逆にどくになってしまいますよ〜」とご指摘をいただきました。ありがとうございます。気づきませんでした...
0000010 //まひ状態の時だけのステータス
^)0000001 //毒消しステータス
0000011 //同じ桁が異なる時だけセットされるのが排他論理和演算子なので、どくになる
使い方がやっぱりちょっと難しいですね。
もう少し理解深まったらまた追記しよう...