盆栽エンジニアリング日記

勉強したことをまとめるブログ

Optionalを実装してみる

自分でOptionalを実装してみる

Javaの勉強として、Optionalを自分で実装してみようと思います。

Optionalの公式ドキュメント

Optionalには以下のメソッドが用意されています。

  • empty
  • equals
  • filter
  • flatMap
  • get
  • hashCode
  • ifPresent
  • isPresent
  • map
  • of
  • ofNullable
  • orElse
  • orElseGet
  • orElseThrow
  • toString

今回は、この中でもよく使うメソッドである以下を実装したMaybeクラスを実装します。

  • empty
  • get
  • isPresent
  • of
  • orElse

StringMaybe

この章では、Genericsを用いないString型専用のMaybeクラスを実装します。
クラスの雛形です。

final public class StringMaybe {
    static StringMaybe empty() {
        return null;
    }

    String get() {
        return null;
    }

    boolean isPresent() {
        return false;
    }

    static StringMaybe of(String value) {
        return null;
    }

    String orElse(String other) {
        return null;
    }
}

StringMaybeクラスでは、String型の変数を保持する必要があるため、最初にメンバ変数を用意します。

private final String value;

valueは、直接外部に公開せず一度設定されたら変更されないようにするために、private finalにしておきます。
このままでは、finalな変数なのに初期化されないため、コンストラクタを用意します。

private StringMaybe(String value) {
        this.value = value;
}

ユーザーがStringMaybeオブジェクトを生成するときは、コンストラクタではなく、staticメソッドを利用してもらうようにするために、コンストラクタにprivate修飾子をつけておきます。
次に、StringMaybeオブジェクトを生成するstaticメソッドを実装していきます。

of

static StringMaybe of(String value) {
       return new StringMaybe(Objects.requireNonNull(value));
}

ofメソッドは、引数で渡されたStringオブジェクトを保持するStringMaybeオブジェクトを返すstaticメソッドです。
valueにnullを許さないように、コンストラクタを呼ぶ際にrequirenonNullを経由させます。

empty

private static final StringMaybe EMPTY = new StringMaybe(null);

static StringMaybe empty() {
        return EMPTY;
}

emptyメソッドは、nullを保持するStringMaybeオブジェクトを返すstaticメソッドです。
このオブジェクトを毎回生成する必要はないので、staticメンバ変数として用意しておいて、毎回そのオブジェクトを返すようにします。
最後に、保持しているStringオブジェクトを取得するget, orElseと、有効なオブジェクトを保持しているか確認するisPresentを実装します。

get

public String get() {
        if (value == null) {
            throw new NoSuchElementException("No value presents");
        }
        return value;
}

getメソッドでは、valueがnullの場合は例外をスローするようにします。

orElse

public String orElse(String other) {
        return value == null ? other : value;
}

orElseメソッドは、valueがnullの場合は、引数で渡されたotherを返します。

isPresent

public boolean isPresent() {
        return value != null;
}

valueがnullでない場合にtrueを返します。
これで、String型専用のMaybeクラスが完成しました。
以下が完成したStringMaybeです。

final public class StringMaybe {
    private final String value;

    private static final StringMaybe EMPTY = new StringMaybe(null);

    private StringMaybe(String value) {
        this.value = value;
    }

    public static StringMaybe empty() {
        return EMPTY;
    }

    public String get() {
        if (value == null) {
            throw new NoSuchElementException("No value presents");
        }
        return value;
    }

    public boolean isPresent() {
        return value != null;
    }

    public static StringMaybe of(String value) {
        return new StringMaybe(Objects.requireNonNull(value));
    }

    public String orElse(String other) {
        return value == null ? other : value;
    }
}

Maybe

この章では、前の章で作成したStringMaybeをGenericsを利用して、String以外にも適用できるようにします。
最初にStringMaybeをコピペして、Maybeクラスを作ります。

final public class Maybe {
    private final String value;

    private static final Maybe EMPTY = new Maybe(null);

    private Maybe(String value) {
        this.value = value;
    }

    public static Maybe empty() {
        return EMPTY;
    }

    public String get() {
        if (value == null) {
            throw new NoSuchElementException("No value presents");
        }
        return value;
    }

    public boolean isPresent() {
        return value != null;
    }

    public static Maybe of(String value) {
        return new Maybe(Objects.requireNonNull(value));
    }

    public String orElse(String other) {
        return value == null ? other : value;
    }
}

次に、Genericsを導入し、Stringを型パラメータで置き換えていきます。

final public class Maybe<T> {
    private final T value;

    private static final Maybe<?> EMPTY = new Maybe<>(null);

    private Maybe(T value) {
        this.value = value;
    }

    public static <T> Maybe<T> empty() {
        @SuppressWarnings("unchecked") Maybe<T> t = (Maybe<T>) EMPTY;
        return t;
    }

    public T get() {
        if (value == null) {
            throw new NoSuchElementException("No value presents");
        }
        return value;
    }

    public boolean isPresent() {
        return value != null;
    }

    public static <T> Maybe<T> of(T value) {
        return new Maybe<T>(Objects.requireNonNull(value));
    }

    public T orElse(T other) {
        return value == null ? other : value;
    }
}

基本的にはクラス定義の際に<T>を追加し、型パラメータTでStringを置き換えるだけですが、staticメソッド内で直接Tを利用することはできないので、その場合はGeneric methodとして定義します。
これでMaybeの実装は完了です。以下のように利用できます。

public class Main {

    public static void main(String[] args) {
        Maybe<String> stringMaybe = Maybe.of("value");
        Maybe<String> emptyStringMaybe = Maybe.empty();
        Maybe<Integer> integerMaybe = Maybe.of(10);

        if (stringMaybe.isPresent()) {
            System.out.println(stringMaybe.get());
        }
        if (!emptyStringMaybe.isPresent()) {
            System.out.println("empty");
            System.out.println(emptyStringMaybe.orElse("orElse"));
        }
        if (integerMaybe.isPresent()) {
            System.out.println(integerMaybe.get());
        }
    }
}

Elmでレコードをソートする

Elmで用意されているList.sort関数は、比較関数を渡すことができず、自分で定義したレコードをソートすることができない。
レコードをソートしたい場合は、以下の関数を利用する

  • sortBy
  • sortWith

List.sortBy

List.sortByの関数定義は以下の通り

sortBy : (a -> comparable) -> List a -> List a

ある型aを受け取って、比較可能な型を返す関数を用意する必要がある。
Elmではレコードを作成すると、.<フィールド名>という関数も作成される。
この関数は、レコードを受け取って、<フィールド名>値を返すため、これをsortByに指定すればいい。

type alias Person =
    { name : String
    , age : Int
    }

List.sortBy .name [Person "person1" 10, Person "person2" 20]
=> [{ age = 10, name = "person1" },{ age = 20, name = "person2" }]

List.sortWith

List.sortWithでは、比較関数を自作することで、更に柔軟にソートを行うことができる。 List.sortWithの定義は以下の通り

sortWith : (a -> a -> Order) -> List a -> List a

Orderは、Elmで定義されているカスタム型で、順序関係を表す。

type Order
    = LT    -- 未満
    | EQ     -- 等しい  
    | GT     -- より大きい

sortWithを使用する場合は、他の言語で比較関数を作成するときのように、2つの変数を受け取って、Orderを返す関数を定義すればいい。

type alias Person =
    { name : String
    , age : Int
    }

compare a b =
    if String.length a.name + a.age > String.length b.name + b.age then
        GT
    else if String.length a.name + a.age < String.length b.name + b.age then
        LT
    else
        EQ

List.sortWith compare [Person "person" 10, Person "person2" 10]
=>[{ age = 10, name = "person" },{ age = 10, name = "person2" }]

参考資料

package.elm-lang.org

vimのペースト時のオートフォーマットを無効にする方法

vimで開いているページに、ソースコードやログをペーストするときに、勝手にインデントや括弧が補完されてしまうことがあります。
長いコードをペーストする際などは、かなり修正がめんどくさくなってしまいます。
これは、以下のコマンドを入力した後に、ペーストすることで防ぐことができます。

:set list

常に、これを有効にしたい場合は、.vimrcにset listと追記すればオーケーです。

Apexの安置解析ツールを作りました。

作ったもの

Apex Legendsにおける、第5ラウンドの安置の出現頻度を解析するサイトを作りました。

apex-calc.herokuapp.com

きっかけ

僕は、ソロでランクマをまわしているのですが、最終安置の傾向がわかれば、より安定してRPを盛れるのではないかと思い作ってみました。
シーズン8の初期から開発をしていたのですが、実際に運用できる段階になったのは、シーズン8のスプリット2に入ってからでした。
スプリット2を終えての集計結果は以下のようになりました。

f:id:t-umino-nit:20210710223935j:plain
シーズン8スプリット1

円が重なるほど色が赤くなる仕様にしました。
開発しているころは、これである程度傾向がわかるかなと思いましたが、実際に集計してみると、特定の安置が突出して多く出現しているわけではありませんでした。
このため、ただ安置をマッピングするだけでは、傾向がよくわかりませんでした。(この画像だと若干右側によっているようにも見えますが、正確な割合はわかりません。)

そこで、新たにマップを9分割して、各エリアにどの程度安置が出現しているかを解析する機能を追加しました。
この機能はシーズン9に入って運用を開始し、以下のような画像も作成できるようになりました。

f:id:t-umino-nit:20210710224728j:plain
シーズン9スプリット1エリア別

この画像によって、最初のマッピング画像だけでは、よくわからなかった安置の傾向が少し分かるようになりました。
嬉しいことに、一部のエリアはかなり出現確率が高いことも確認できました。
ここまで来ると、ジャンプ先やマッチ中の移動経路の検討にもかなり使えるようになってきたと思います。

集計への参加方法

今回作ったシステムは、安置の解析を、画像のアップロード以外完全自動で行います。
また、画像のアップロードはtwitterハッシュタグ#apex_mapを付けてツイートするだけなので、誰でも集計に参加することができます。
具体的な参加手順は以下の通りです。

  1. 第6ラウンドのマップのスクリーンショットを撮る
  2. twitterハッシュタグ#apex_mapを付けてアップロードする

f:id:t-umino-nit:20210710225456j:plain
マップ画像の例

Apex Legendsのランクマッチで使えるツールを作りました。

こんなwebサイトを作りました。 apex-calc.herokuapp.com

自粛期間中にApex Legendsというゲームを初めました。
1チーム3人構成で全20チームで争うバトルロワイヤルです。
このApex Legendsにはランクマッチというモードがあり、日本中のプレイヤーが毎日競い合っています。
もともと、スマブラや格ゲーをやっていた経験があったため、僕もすぐにこのランクマッチに夢中になりました。
最初は人生初FPSだったので、苦戦していましたが、プレイし始めてから5ヶ月ほどで全プレイ人口の3%のほどのダイヤランクに到達することができました。
f:id:t-umino-nit:20201224231629j:plain

Apex Legendsのランクマッチは、ランクに応じた参加費を支払ってマッチに参加し、マッチで得られたポイントと参加費の差額を積み重ねていくシステムです。
マッチで得られるポイントには、敵をキルしたり味方のキルをアシストすることで得られるポイント(以下キルポ)と、順位点の2種類があります。
普段は特に気にならないのですが、次のランクの昇格が近づいてくると、どのくらいの順位とキルポがあれば昇格できるのかが気になってきます。
Apex Legendsではキルポに順位補正がかかるので、計算が少し複雑になり、そのたびに必要順位等を考えるのは少し面倒です。
そこで、欲しいポイントを入力すると、そのポイントを得るために必要な順位とキルポを計算してくれるツールを作成しました。

apex-calc.herokuapp.com

使い方は至ってシンプルで、ページ内の入力欄に必要なポイントを入力し、計算ボタンを押すだけです。
よかったら使ってみてください!
それでは良いApexライフを

ABC173自分なりのまとめ

A- Payment

N円を超えるまで1000円ずつ足していけば良いです。

//競技プログラミング用のテンプレート
#include <iostream>
#include <vector>
#include <algorithm>
#include <map>
#include <queue>
#include <string>
#include <math.h>
#include <stack>
#include <limits>

#define rep(i,n) for(int i=0, i##_len=(n); i<i##_len; ++i)
#define repr(i, n) for(int i = n - 1; i >= 0; i--)

using namespace std;
//エイリアス
using ll = long long int;
using p = pair<int, int>;
using pl = pair<ll, ll>;
using v = vector<int>;
using vd = vector<double>;
using vs = vector<string>;
using vl = vector<ll>;

//定数
const int intmax = numeric_limits<int>::max();
const ll llmax = numeric_limits<ll>::max();
const ll mod = 1e9 + 7;
const double pi = M_PI;

int main()
{
  int n;
  cin >> n;
  int pay = 1000;
  while(pay < n) {
    pay += 1000;
  }
  cout << pay - n << endl;
}

B- Judge Status Summary

mapにそれぞれの出現回数を記録しました。

//競技プログラミング用のテンプレート
#include <iostream>
#include <vector>
#include <algorithm>
#include <map>
#include <queue>
#include <string>
#include <math.h>
#include <stack>
#include <limits>

#define rep(i,n) for(int i=0, i##_len=(n); i<i##_len; ++i)
#define repr(i, n) for(int i = n - 1; i >= 0; i--)

using namespace std;
//エイリアス
using ll = long long int;
using p = pair<int, int>;
using pl = pair<ll, ll>;
using v = vector<int>;
using vd = vector<double>;
using vs = vector<string>;
using vl = vector<ll>;

//定数
const int intmax = numeric_limits<int>::max();
const ll llmax = numeric_limits<ll>::max();
const ll mod = 1e9 + 7;
const double pi = M_PI;

string cand[] = {"AC", "WA", "TLE", "RE"};

int main()
{
  int n;
  cin >> n;
  map<string, int> memo;
  rep(i, n)
  {
    string s;
    cin >> s;
    memo[s]++;
  }
  rep(i, 4) {
    printf("%s x %d
", cand[i].c_str(), memo[cand[i]]);
  }
}

C- H and V

H + Wビットのbit全探索を行えばよいです。
H,Wともに6以下なので計算量的にも問題ありません。

//競技プログラミング用のテンプレート
#include <iostream>
#include <vector>
#include <algorithm>
#include <map>
#include <queue>
#include <string>
#include <math.h>
#include <stack>
#include <limits>

#define rep(i,n) for(int i=0, i##_len=(n); i<i##_len; ++i)
#define repr(i, n) for(int i = n - 1; i >= 0; i--)

using namespace std;
//エイリアス
using ll = long long int;
using p = pair<int, int>;
using pl = pair<ll, ll>;
using v = vector<int>;
using vd = vector<double>;
using vs = vector<string>;
using vl = vector<ll>;

//定数
const int intmax = numeric_limits<int>::max();
const ll llmax = numeric_limits<ll>::max();
const ll mod = 1e9 + 7;
const double pi = M_PI;

string grid[6] = {""};

void choose(map<int, int> &memo, int bits, int &comp, int size) {
  for(int i = 0; i < size; i++) {
    if (bits & comp) {
      memo[i] = 1;
    }
    comp = comp << 1;
  }
}

int count(int h, int w, map<int, int> &row, map<int, int> &col) {
  int result = 0;
  for(int i = 0; i < h; i++)
  {
    for(int j = 0; j < w; j++)
    {
      if (row.count(i) == 0) continue;
      if (col.count(j) == 0) continue;
      if (grid[i][j] == '#') result++;
    }
  }
  return result;
}

int main()
{
  int h, w, k;
  cin >> h >> w >> k;
  rep(i, h)
  {
    cin >> grid[i];
  }
  int cand = 0;
  for(int i = 0; i < pow(2, h + w); i++) {
    int comp = 1;
    map<int, int> row;
    map<int, int> col;  
    choose(row, i, comp, h);
    choose(col, i, comp, w);
    int temp = count(h, w, row, col);
    if (temp == k) cand++;
  }
  cout << cand << endl;
}

D- Chat in a Circle

結構難しめかな?
最適性の証明が難しかったです。(今もできてるか怪しい)
直感的には思いつきやすいのではないでしょうか
最初にプレイヤーをフレンドリーさの降順にソートします。
その後、それぞれのフレンドリーさを何回心地よさに加えることができるかを考えます。
一番大きいフレンドリーさA{max}は1回しか加えることができません。
これは、心地よさに加えられるフレンドリーさは両脇のうち小さい方だからです。
では、その次に大きいフレンドリーさは何回加えられるでしょうか?
フレンドリーさの降順に到着していくと仮定すると、最大2回加えられることがわかります。
右隣にプレイヤーを到着させた場合と、左隣にプレイヤーを到着させた場合です。
フレンドリーさの降順に到着していくと仮定しているので、右隣と左隣、一回ずつしか加えられません。
この場合、最終的な心地よさの合計は、A
{max} + 2 * A_i (i != max)となります。
次に、この到着の仕方が最適であることを証明します。
もしあるフレンドリーさA_iを2回よりも多く心地よさに加えようとすると、プレイヤーiよりもあとに到着するプレイヤーの フレンドリーさの方が大きい必要があります。
すると、A_iを余分に加えることができたメリットよりも、後に到着したプレイヤーのフレンドリーさを加えられなかったデメリットのほうが 大きくなるため、フレンドリーさの降順に到着するのが最適だとわかります。

//競技プログラミング用のテンプレート
#include <iostream>
#include <vector>
#include <algorithm>
#include <map>
#include <queue>
#include <string>
#include <math.h>
#include <stack>
#include <limits>

#define rep(i,n) for(int i=0, i##_len=(n); i<i##_len; ++i)
#define repr(i, n) for(int i = n - 1; i >= 0; i--)

using namespace std;
//エイリアス
using ll = long long int;
using p = pair<int, int>;
using pl = pair<ll, ll>;
using v = vector<int>;
using vd = vector<double>;
using vs = vector<string>;
using vl = vector<ll>;

//定数
const int intmax = numeric_limits<int>::max();
const ll llmax = numeric_limits<ll>::max();
const ll mod = 1e9 + 7;
const double pi = M_PI;

int main()
{
  int n;
  cin >> n;
  vl a(n, 0);
  rep(i, n) cin >> a[i];
  sort(a.begin(), a.end());
  reverse(a.begin(), a.end());
  ll sum = a[0];
  int arrived = 2;
  for(int i = 1; i < n && arrived <= n; i++) {
    if (arrived + 1 <= n) {
      sum += a[i];
      arrived++;
    }
    if (arrived + 1 <= n) {
      sum += a[i];
      arrived++;
    }
  }
  cout << sum << endl;
}

E- Multiplication 4

コンテスト中には解けなかったけど、D問題よりも考察自体は簡単かも
基本的に、絶対値の降順にk個取ったとき、掛け算の結果がマイナスにならなければそれが解になります。
解がマイナスになった場合は

  • 負の数をK個の中から一つ取り除いて、正の数を追加する
  • 正の数をK個の中から一つ取り除いて、負の数を追加する

ことで、結果を正にすることができます。
どちらの方法をとったほうが解が大きくなるかは

  • R: 先頭からK個取ったときの解
  • P1: K個の中から最も小さい正の数
  • M1: K個の中から最も絶対値が小さい負の数
  • P2: 残った数の中から最も大きい正の数
  • M2: 残った数の中から最も絶対値が大きい負の数

とすると

  • P1 * P2 < M1 * M2

で比較することができます。
あとは細かい例外を適切に処理していけば解けるはずです。
コードが説明と若干離れているのは目をつぶってください。

//競技プログラミング用のテンプレート
#include <iostream>
#include <vector>
#include <algorithm>
#include <map>
#include <queue>
#include <string>
#include <math.h>
#include <stack>
#include <limits>

#define rep(i,n) for(int i=0, i##_len=(n); i<i##_len; ++i)
#define repr(i, n) for(int i = n - 1; i >= 0; i--)

using namespace std;
//エイリアス
using ll = long long int;
using p = pair<int, int>;
using pl = pair<ll, ll>;
using v = vector<int>;
using vd = vector<double>;
using vs = vector<string>;
using vl = vector<ll>;

//定数
const int intmax = numeric_limits<int>::max();
const ll llmax = numeric_limits<ll>::max();
const ll llmin = numeric_limits<ll>::min();
const ll mod = 1e9 + 7;
const double pi = M_PI;

ll calc_only(vl &src, int k) {
  ll ans = 1;
  for(int i = 0; i < k; i++) {
    ans *= src[i];
    ans %= mod;
  }
  return ans;
}

ll calc_mix_even(vector<pair<ll, int>> &mix,  int k) {
  ll ans = 1;
  for(int i = 0; i < k; i++) {
    ans *= mix[i].first;
    ans %= mod;
  }
  return ans;
}

ll calc_mix_odd_without(vector<pair<ll, int>> &mix, int k, int target) {
  ll ans = 1;
  for(int i = 0; i < k; i++) {
    if (i == target) continue;
    ans *= mix[i].first;
    ans %= mod;
  }
  return ans;
}

int get_last_index(vector<pair<ll, int>> &mix, int k, int target) {
  int last_index = -1;
  for(int i = 0; i < k; i++)
  {
    if(mix[i].second == target) last_index = i;
  }
  return last_index;
}

int get_first_index(vector<pair<ll, int>> &mix, int k, int target) {
  for(int i = k; i < mix.size(); i++)
  {
    if(mix[i].second == target) return i;
  }
  return -1;
}

ll adapt(vector<pair<ll, int>> &mix, int k, int without, int add) {
  ll ans = calc_mix_odd_without(mix, k, without);
  ans *= mix[add].first;
  ans %= mod;
  return ans;
}

ll calc_mix_odd(vector<pair<ll, int>> &mix, int k) {
  ll ans = llmin;
  int plus_last = get_last_index(mix, k, 1);
  int minus_last = get_last_index(mix, k, -1);
  int plus_first = get_first_index(mix, k, 1);
  int minus_first = get_first_index(mix, k , -1);
  if (plus_last == -1) {
    //プラスが<kにそもそも存在しない
    //マイナスを取り除いてプラスを入れる
    return adapt(mix, k, minus_last, plus_first);
  }
  if (plus_first == -1) {
    //プラスが>=kに存在しない
    //プラスを取り除いてマイナスを入れる
    return adapt(mix, k, plus_last, minus_first);
  }
  if (minus_first == -1) {
    //マイナスが>=kに存在しない
    //マイナスを取り除いてプラスを入れる
    return adapt(mix, k, minus_last, plus_first);
  }
  ll plus1 = mix[plus_last].first;
  ll minus1 = mix[minus_last].first;
  ll plus2 = mix[plus_first].first;
  ll minus2 = mix[minus_first].first;
  if (plus1 * plus2 < minus1 * minus2) {
    //プラスを取り除いてマイナスを入れる
    return adapt(mix, k, plus_last, minus_first);
  } else {
    //マイナスを取り除いてプラスを入れる
    return adapt(mix, k, minus_last, plus_first);
  }
}


ll calc_mix(vector<pair<ll, int>> &mix, int k) {
  int num_minus_in_k = 0;
  for(int i = 0; i < k; i++) {
    if (mix[i].second == -1) num_minus_in_k++;
  }
  if (num_minus_in_k % 2 == 0) {
    return calc_mix_even(mix, k);
  }
  if (num_minus_in_k % 2 != 0) {
    return calc_mix_odd(mix, k);
  }
}


int main()
{
  int n, k;
  cin >> n >> k;
  vl as(n, 0);
  vl plus, minus, minusr;
  vector<pair<ll, int>> abss(n);
  rep(i, n) {
    ll a; cin >> a;
    as[i] = a;
    if (a < 0) {
      minus.push_back(a);
      abss[i] = {abs(a), -1};
    }else{
      plus.push_back(a);
      abss[i] = {a, 1};
    }
  }
  sort(plus.begin(), plus.end());
  reverse(plus.begin(), plus.end());
  sort(minus.begin(), minus.end());
  minusr = minus;
  reverse(minusr.begin(), minusr.end());
  sort(abss.begin(), abss.end());
  reverse(abss.begin(), abss.end());
  //全部正
  if (plus.size() == n) {
    ll ans = calc_only(plus, k);
    cout << ans << endl;
    return 0;
  }
  //全部負
  if (minus.size() == n) {
    ll ans = llmin;
    if (k % 2 == 0) ans = calc_only(minus, k);
    if (k % 2 != 0) ans = calc_only(minusr, k);
    if (ans < 0) ans += mod;
    cout << ans << endl;
    return 0;
  }
  //n == k
  if (n == k) {
    ll ans = calc_only(as, k);
    if (ans < 0) ans += mod;
    cout << ans << endl;
    return 0;
  }
  //それ以外
  ll ans = calc_mix(abss, k);
  cout << ans << endl;
}

格ゲーマーが他ゲーを語る際の傲慢さについて

なんか色々と吐き出したくなったので、グダグダ書いていきます。

きっかけ

格ゲーのプロプレイヤーの配信を見ていると、たまになぜ格ゲーが流行らないのかといった話題になることがあります。
自分はそのような話題になったときのコメントや配信者の発言について、ずっと違和感を抱いていました。
具体的にどのようなコメントが書かれるのかというと

  1. FPSやTPSは人のせいにできる
  2. 格ゲーはすべて自己責任
  3. 最近のゲーマーはそれに耐えられない

といった感じです。
1については、スプラトゥーンやApexなどのチーム制のゲームが流行っているから書かれるのでしょう。
こういうコメントや発言に接するたびに、なんとなくモヤモヤしていましたが、最近ある動画を見てその感情が爆発しました。 問題の動画はこれです。
これから、この動画を見て考えたことをつらつら書いていこうと思います。

プレイ遍歴

最初に自分がこれまでどんなゲームをやってきたのかを書いておこうと思います。
自分がプレイしたことのある対戦ゲームは以下の通りです。

スマブラは、メインキャラとしてフォックスを使用していて、VIPマッチに参加できるくらいの実力です。
スマブラに少し飽きてきたときにストVに手を出してシルバーになるまでやりました。
その後、グラブルVSが発売されたので1ヶ月くらいプレイしましたが、就職でゴタゴタしてるうちにやらなくなりました。
最近落ち着いてきたのでApexに手を出して1ヶ月くらいやってます。

なにに怒っているのか

自分が上の動画や、格ゲーマー達の発言を聞いて思うのは、なぜこの人達はこんなにも上から目線なのだろうということです。
格ゲーではなく他のゲームが流行っている理由を、そのゲームの面白みなどから考えるのではなく、他人のせいにできるからと断言し、それに比べて格ゲーはすべて自己責任だから普通の人は耐えられないのだと彼らは言うのです。
正直、呆れてしまいます。
人は他人のせいにできるから、そのゲームをプレイするのでしょうか?
違うでしょ。楽しいからプレイするんでしょ。
そもそも、格ゲーがすべて自己責任というのも怪しいと思います。
格ゲーマーってよく特定のキャラの文句言ってますよね?
それって自分が負ける原因を自分以外の要因に押し付けてませんか?
それでよくこんなこと言えるよなと思います。
格ゲーが流行らない他の理由としては、

  • 難しいから
  • 初心者が勝てないから

もよく挙げられます。
更に踏み込んでFPSが流行ってるのは簡単だからとか、初心者でも上級者に勝てるからとか言ってる人も見かけます。
この人達FPSやったことあるんですかね。
自分もそんなにやり込んではないですけど、FPSめちゃくちゃ難しいと思います。
確かにボタンを押せば弾が撃てるし、基本操作はコマンド等を必要としないから簡単というのは正しいかもしれません。
ただこれはゲームが簡単ということにはなりません。
敵の位置把握や立ち回り等々難しい要素はいくらでもあると思います。
なんで自分たちがやっているゲームがそんなに特別だと思えるんでしょうか。
初心者が上級者に勝てるというのも怪しいと思います。
1対1の打ち合いになったら初心者はまず勝てないと思います。
もちろん、不意打ちできたら倒せることはありますが、初心者がそうそう毎回裏をとったり漁夫ったりはできません。
毎回できるならそれはもう初心者ではないと思います。
初心者が勝てないというのも、今はもうそんなことはないと思います。
これはどのゲームでもある程度、実力に応じたマッチングが行われているためです。
実際に、自分がストVを始めたときも一切勝てないということはありませんでした。

ここまで色々と書いてきましたが、とにかく格ゲーマーに対して言いたいことは、傲慢すぎやしませんかということです。
別に格ゲーは特別なゲームジャンルではないと思います。
今流行っていない原因を、自分たちに他のゲーマーがついてこれないからだというような言論はやめませんか?
そんなことをしても格ゲーの未来は明るくなりませんよ。

格ゲーはなぜ流行らないのか

ここからは格ゲーがなぜ流行っていないのかについて、自分が思うところを書いていきます。
ここで明確にしておきますが、自分は格ゲーは好きです。
今はプレイしていませんが、ギルティギアの新作出たら手を出してみようとも思っています。
なので、格ゲー人口がもっと増えて盛り上がって欲しいと思っています。
決して格ゲーというジャンルそのものを批判したいわけではありません。
閑話休題
まず、格ゲーの現状ですが、ストVとApexをGoogleトレンドで比べるとこんな感じです。

f:id:t-umino-nit:20200708035427p:plain
トレンド
正直勝負になっていません。
これは何が原因なのでしょうか

知名度が低い

まず圧倒的に知名度が低いと思います。
プレイしたことがある人は同世代では全くいません。
ちなみに、自分は今年就職しましたが、プロフィールにFPSが趣味と書く人はいても、格ゲーやストVとか書いてる人は一人もいませんでした。
ちょっと前に、ある格ゲーマーが自動車免許を取りに合宿に行くという話を番組でしていました。
その際に、彼は1人ぐらい自分のことを知っている人がいそうで嫌だという話をして番組でも盛り上がっていました。
しかし、自分の周りを見渡してみると正直知っている人なんていないだろうなと思いました。
(確認していませんが、もし本当にいたならばすいません)
格ゲーのプロゲーマーで知られている人なんて、梅原さんとときどさんくらいだと思います。
それくらい、格ゲー界隈と世間ではギャップがあると思います。
おそらく大多数の人は、ストリートファイター、スト2といった単語だったら聞いたことがある程度が関の山な気がします。
実際、自分もスマブラを始めるまでは、なんか昔流行ったらしいぐらいの認識しかありませんでした。
プレイ画面をほとんど見たことがなかったと思います。
格ゲー全盛期(正直はるか昔?)ならば、新規参入する人も多かったのかもしれませんが、現状全くと言っていいほど認知されていないと思うので、そこから頑張ったほうがいいと思います。
とにかく面白そうだと思ってもらえる取り組みが足りないんじゃないかなと思います。
クソ長いロード時間にリーグの宣伝するよりも、潜在層にアピールしてほしい。 FPSやTPSは圧倒的な知名度と、後述する協力プレイのおかげで、連鎖的に人口が増加していると思います。

アクションゲームとしての面白さ

次にアクションゲームとしての面白さです。
スマブラから格ゲーというものを知った自分は、動画などを見てこれは面白そうだと思って格ゲーを始めてみましたが、初めてキャラを動かしたとき愕然としました。
それは全くアクションゲームとしての面白さや気持ちよさが感じられなかったからです。
スティックを倒しても、キャラがジリジリ動くだけ
ストVは走ることもできない
ジャンプはこのご時世に魔界村方式
正直、このプレイ感覚ではパっと触って面白くないなと思ってやめてしまう人もいるのではないかと思います。
もう少し、キャラを動かすだけ楽しさを感じられるようにしてほしいです。
ただ必殺技キャンセルを交えたコンボを決める楽しさは唯一無二だと感じました。
初心者にそこまで到達してもらうためにも、まずアクションゲームとして面白いものにしたほうが良いと思います。
特にAPEXを始めて思ったのですが、基本的な動作がどれも気持ちいです。
銃声や、弾があたったときの効果音や振動が心地よいフィードバックを与えてくれます。
ただ的に向かって銃を撃ってるだけでも、結構面白いんです。
全体的に動作にスピード感があるおかげで、チュートリアルで走ってスライディングしてジャンプするだけでも爽快感があります。
格ゲーに足りないのはこういう部分なのかなと思います。

シングルプレイがつまらない

これストVの愚痴になっちゃうんですが、ストーリーモードみたいなやつつまらなすぎですよね
面白くもないストーリーを静止画で見せられて機械的に対戦をこなしていくだけ
もっとここにも力を入れてほしい

有料、キャラ解放にも課金が必要

これもストV(以下略)
最近流行ってるゲームには、無料で配信されているものも多いです。
ストVをはゲーム内通貨でキャラ開放を行うのは非常に難しい(2019年の話)のに対してApexは普通にプレイしてればどんどん解放できます。
どうせコスチュームで稼ぐなら、いっそのこと無料にしてみてはどうでしょうか。

協力プレイや多人数プレイができない

最後に協力プレイや多人数プレイができないことですが、正直これが一番でかい気がします。
格ゲーって友達が複数人集まってできないんですよね。
オフラインだと、家にモニターとゲーム機が複数ある人なんてそうそういないですよね。
せっかく集まってるのに対戦してる2人以外は見てることしかできない。
だったら、みんなで遊べるゲームを選びますよね。
オンラインはロビー機能はありますが、ゲーム自体にボイチャ機能がなかったり(これあったらすいません)して、協力ゲームに比べて充実しているとは思えません。
昔はゲームセンターが交流の場としての役割を担っていたのだと思いますが、現状は代替が存在しないように思えます。
もう少しロビーが仮想的なゲームセンターのようになって、そこに友人で集まるような感じになると良いと思います。
やはり協力や多人数でプレイできるゲームは、圧倒的に広まりやすいと思うので、その効果を格ゲーも取り込むべきだと思います。

最後に

バーっと書いていきましたが、だいぶスッキリしました。
過去の栄光を引きずって他に流行ってるゲームとそのプレイヤーをくさすのではなく、どうやったら時代に適応していきるのかを考えるべきじゃないかな。