プログラミング

連続の技習得編 オブジェクトをセッションへ格納 PHPポケモン57

PHP PHPポケモン ポケモン
連続の技習得編 オブジェクトをセッションへ格納 PHPポケモン57

セッション経由でのオブジェクト引き継ぎ

技習得の処理が整ってきたので、ここで連続技習得・連続レベルアップ時にも問題なく動作するように作り込んでいきます。ですが、現状のモーダルをレスポンスやメッセージと同様に、そのまま引き継いだとしてもエラーが発生します。

その原因がセッション経由でのオブジェクト渡しです。

PHPポケモン「オートロード編(修正版)」17 おまけ:日本語化 PHPポケモン「オートロード編(修正版)」17 おまけ:日本語化

  前回実装したオートローダーの使い方が盛大に間違っていたので、今回その間違いの説明をしながら、正しい実装方法をご紹介します。 申し訳ありません。(誠意)    オートロードについて(再)  必要なタイミングで必要なファイルをrequireまたはincludeするあれです。   前回spl_autoload_reg...

 

第17回で取り上げましたが、オブジェクトはセッションで次のページへ受け渡しすると、不完全なオブジェクト(__PHP_Incomplete_Class)になります。技習得時のレスポンスにオブジェクトは格納されていませんでしたが、モーダルでは新しい技のオブジェクトなどが含まれていますね。

 

現状のシステム構成のまま進めていくのであれば、解決策は2通りです。

  1. オブジェクトではなく配列形式で受け渡しをする
  2. オブジェクトをシリアライズする

 

まず、配列形式での受け渡しについてです。こちらは、現在のポケモン情報のようにexportなどのメソッドを用意して、画面移管後に新しくインスタンスを生成する方法です。

ですがポケモンのオブジェクトとは違い、レスポンスやモーダルは今後も増えていく可能性が高いです。その都度、配列形式を意識したデータの受け渡しをするとなれば大変ですね。

 

なので、今回は「オブジェクトのシリアライズ」で実装していきましょう。

 

シリアライズ(文字列化)

まず、シリアライズについて簡単に説明しておきます。PHPの関数を使って、オブジェクトを文字列化させることができます。jsonに似たような形式です。

シリアライズすれば、受け取り画面ではアンシリアライズの関数を実行することでオブジェクトを復元することが可能です。

 

今回使用するシリアライズ用の関数はserializeです。

 

一括でシリアライズさせるために、トレイトを用意してコントローラーとサービスで呼び出せるようにしておきましょう。

 

シリアライズトレイト(/App/Traits/SerializeTrait.php
<?php
 
trait SerializeTrait
{
    /**
    * オブジェクトのシリアライズ化
    * @param arg:mixed
    * @return mixed
    */
    public function serializeObject($arg)
    {
        if(is_array($arg)){
            // 配列の場合はループ
            $result = [];
            foreach($arg as $key => $val){
                $result[$key] = $this->serializeObject($val);
            }
            $arg = $result;
        }else if(is_object($arg)){
            // オブジェクトの場合はシリアライズ
            $arg = ['__serialize' => serialize($arg)];
        }
        return $arg;
    }
}

 

一括の場合は配列、単体の場合はオブジェクトで受け取ることになるので、引数はmixed想定で作成しています。

 

もし配列であれば、foreachを使って再度serializeObjectを呼び出しています。

受け取った値がオブジェクトであれば、serialize関数で文字列に変換します。ここで、そのまま返してしまうと復号化(アンシリアライズ)させる時に「どれを復号化すれば良いのか」がわからなくなってしまいます。個別で行なっても構いませんが、できれば一括で復号化させたいので、配列形式で返却しましょう。判別用として、キーを「__serialize」としています。

 

コントローラーとサービスの親クラスでトレイトを読み込み、格納時にシリアライズ化しましょう。

 

バトル画面ヘッド(/Resources/Partials/Layouts/Head/battle.php
// レスポンスはシリアライズ化
$_SESSION['__data']['before_reponses'] = $controller->serializeObject(
    $controller->getResponses()
);
$_SESSION['__data']['before_messages'] = $controller->getMessages();

 

現状、メッセージにはオブジェクトを格納することがありませんので、レスポンスのみシリアライズさせています。

 

アンシリアライズ(復号化)

次にアンシリアライズ(復号化)についてです。こちらもシリアライズトレイト内に、配列形式を想定してメソッドを作成しましょう。

 

シリアライズトレイト(/App/Traits/SerializeTrait.php
/**
* オブジェクトのアンシリアライズ化
* @param arg:mixed
* @return mixed
*/
public function unserializeObject($arg)
{
    // 配列の処理
    if(is_array($arg)){
        if(
            count($arg) === 1
            && isset($arg['__serialize'])
        ){
            // 配列の中身がシリアライズされたオブジェクトであればオブジェクト化
            $arg = unserialize($arg['__serialize']);
        }else{
            // 通常配列の場合はループ
            $result = [];
            foreach($arg as $key => $val){
                $result[$key] = $this->unserializeObject($val);
            }
            $arg = $result;
        }
    }
    return $arg;
}

 

配列の場合はループをさせていますが、もし配列内の個数が1つ__serializeというキーを持っていれば、unserialize関数を呼び出しています。

 

アンシリアライズ化のメソッドは、サービスでデータを受け取った際に使用します。

 

技習得用サービス(/App/Serivces/Battle/LearnMoveService.php
/**
* @param Pokemon:object
* @param before_response:array
* @param before_messages:array
* @param request:array
* @return void
*/
public function __construct($pokemon, $before_responses, $before_messages, $request)
{
    $this->pokemon = $pokemon;
    $this->before_responses = $this->unserializeObject($before_responses);
    $this->before_messages = $before_messages;
    $this->before_modals = $this->unserializeObject($before_modals);
    $this->request = $request;
}

 

これでオブジェクトの状態でデータを引き継ぐことができます。

※シリアライズする際は、データ改ざんの可能性を想定してハッシュ化させて照合したり、様々な対策をした上で行うことが推奨されています。また、現在はjsonを使ったデータの受け渡しが主流かつ安全と言われているため、本格的なシステム開発では十分注意して導入しましょう。

  

モーダルの引き継ぎ

それでは、モーダルの引き継ぎ処理を作成していきましょう。

 まずは、レスポンスやメッセージと同様に前回情報をセッションに格納し、技習得モーダルで受け取ります。

 

バトル画面ヘッド(/Resources/Partials/Layouts/Head/battle.php
//モーダルはシリアライズ化
$_SESSION['__data']['before_modals'] = $controller->serializeObject(
    $controller->getModalss()
);

 

バトルコントローラー(/App/Controller/Battle/BattleController.php
// branchメソッドswitch分岐
/******************************************
* 技の習得
*/
case 'learn_move':
// サービス実行
$service = new LearnMoveService(
    $this->pokemon,
    $_SESSION['__data']['before_reponses'],
    $_SESSION['__data']['before_messages'],
    $_SESSION['__data']['before_modals'],
    $this->request('param')
);
$service->execute();
// 描画するポケモン情報を置き換え
$this->before['friend'] = $service->getTmpPokemon();
break;

 

技習得用サービス(/App/Serivces/Battle/LearnMoveService.php
<?php
$root_path = __DIR__.'/../../..';
// 親クラス
require_once($root_path.'/App/Services/Service.php');
// トレイト
require_once($root_path.'/App/Traits/Service/Battle/ServiceBattleCheckTrait.php');
 
/**
* 技の習得処理
*/
class LearnMoveService extends Service
{
 
    use ServiceBattleCheckTrait;
 
    /**
    * @var Pokemon:object
    */
    protected $pokemon;
 
    /**
    * @var Pokemon:object
    */
    protected $tmp_pokemon;
 
    /**
    * @var array
    */
    protected $before_responses;
 
    /**
    * @var array
    */
    protected $request;
 
    /**
    * @param Pokemon:object
    * @param before_response:array
    * @param before_messages:array
    * @param before_modals:array
    * @param request:array
    * @return void
    */
    public function __construct($pokemon, $before_responses, $before_messages, $before_modals, $request)
    {
        $this->pokemon = $pokemon;
        $this->before_responses = $this->unserializeObject($before_responses);
        $this->before_messages = $before_messages;
        $this->before_modals = $this->unserializeObject($before_modals);
        $this->request = $request;
    }
 
    /**
    * @return void
    */
    public function execute()
    {
        // 描画用ポケモンオブジェクトの作成
        $this->tmp_pokemon = $this->createTmpPokemon();
        // 技の置き換え
        $this->replaceMove();
        // レスポンスの引き継ぎ
        $this->setResponse(
            $this->getUntreatedResponses($this->before_responses, $this->request['id'])
        );
        // メッセージの引き継ぎ
        $this->setMessage(
            $this->getUntreatedResponses($this->before_messages, $this->request['id'], 'message')
        );
        // モーダルの引き継ぎ
        $this->setModal(
            $this->getUntreatedResponses($this->before_modals, $this->request['id'], 'modal'), true
        );
    }
 
    /**
    * @return Pokomon:object
    */
    public function getTmpPokemon()
    {
        return $this->tmp_pokemon;
    }
 
    /**
    * 表示用のポケモンオブジェクトを生成
    * @return Pokemon:object
    */
    private function createTmpPokemon()
    {
        $pokemon = clone $this->pokemon;
        // クローンオブジェクトにレベルと残HPをセット
        $pokemon->setLevel($this->request['level']);
        $pokemon->setRemainingHp($this->request['hp']);
        $pokemon->setDefaultExp();
        return $pokemon;
    }
 
    /**
    * 技の置き換え
    * @return void
    */
    private function replaceMove()
    {
        // 忘れる技を取得
        $forget_move = $this->pokemon
        ->getMove($this->request['forget']);
        // 覚えさせる技を取得
        $new_move = new $this->before_responses[$this->request['id']]['move'];
        // 技を覚えさせる
        $this->pokemon
        ->setMove($new_move, $this->request['forget']);
        // メッセージの返却
        $this->setMessage('1 2の ……ポカン!');
        $this->setMessage($this->pokemon->getNickname().'は、'.$forget_move->getName().'の使い方をキレイに忘れた!そして......');
        $this->setMessage($this->pokemon->getNickname().'は新しく、'.$new_move->getName().'を覚えた!');
    }
 
    /**
    * 未処理レスポンス・メッセージ・モーダルの引き継ぎ処理
    * @param response:array
    * @param msg_id:string
    * @param param:string::response|message|modal
    * @return array
    */
    private function getUntreatedResponses(array $responses, string $msg_id, string $param='response')
    {
        $cnt = 1;
        switch ($param) {
            /********
            * メッセージの引き継ぎ
            */
            case 'message':
            $key = array_search(
                $msg_id,
                array_column($responses, 1), # メッセージIDの位置は1番目
                true
            );
            // 対象メッセージを含め3つ目までを削除
            $cnt = 3;
            break;
            /********
            * モーダルの引き継ぎ
            */
            case 'modal':
            $key = array_search(
                $msg_id,
                array_column($responses, 0), # メッセージIDの位置は0番目
                true
            );
            break;
            /********
            * レスポンスの引き継ぎ
            */
            default:
            // メッセージIDのレスポンスが入った位置を取得
            $key = array_search(
                $msg_id,
                array_keys($responses),
                true
            );
            break;
        }
        // 未処理だけを切り出して返却
        return array_splice($responses, $key + $cnt);
    }
 
}

 

現在覚えている技

ここから前回までに実装した機能の修正箇所です。まずは技モーダルの返却値についてです。

 

ポケモンクラスチェック用トレイト(/App/Traits/Class/Pokemon/ClassPokemonCheckTrait.php
// checkLevelMoveメソッド(旧:checkMove)
 
// モーダル用のレスポンスをセット
$this->setModal([
    'id' => $msg_id,
    'modal' => 'selectmove',
    'new_move' => $move
]);

 

前回は現在覚えている技に新しい技をマージして返却していましたが、それでは連続して技を覚える際に、新しく覚えた技が反映されていないオブジェクトが格納されたままになります。

これを回避するために、モーダルには現在の技+返却された技を出力させます。

 

忘れさせる技選択用モーダル(/Resources/Partials/Battle/Modals/selectmove.php
<div class="modal-body">
    <?php # 覚えている技 ?>
    <table class="table table-bordered table-sm table-hover">
        <thead class="thead-light">
            <tr>
                <th scope="col">覚えている技</th>
                <th scope="col">タイプ</th>
                <th scope="col">PP</th>
            </tr>
        </thead>
        <tbody>
            <?php foreach($pokemon->getMove() as $key => $move): ?>
                <tr class="move-detail-link forget-selectmove <?php if($key === 4) echo 'active new-move'; ?>"
                    data-modal="#<?=$modal['id']?>-modal"
                    data-target="#<?=$modal['id']?>_<?=get_class($move['class'])?>-content"
                    data-name="<?=$move['class']->getName()?>"
                    data-num="<?=$key?>">
                    <th scope="row" class="w-50"><?=$move['class']->getName()?></th>
                    <td><?=$move['class']->getType()->getName()?></td>
                    <td><?=$move['remaining']?>/<?=$move['class']->getPp($move['correction'])?></td>
                </tr>
            <?php endforeach; ?>
            <?php # 覚えようとしている技 ?>
            <tr class="move-detail-link forget-selectmove active new-move"
                data-modal="#<?=$modal['id']?>-modal"
                data-target="#<?=$modal['id']?>_<?=get_class($modal['new_move'])?>-content"
                data-name="<?=$modal['new_move']->getName()?>"
                data-num="<?=$key?>">
                <th scope="row" class="w-50"><?=$modal['new_move']->getName()?></th>
                <td><?=$modal['new_move']->getType()->getName()?></td>
                <td><?=$modal['new_move']->getPp()?>/<?=$modal['new_move']->getPp()?></td>
            </tr>
        </tbody>
    </table>
    <?php # 技説明 ?>
    <div class="overflow-auto p-3 border" style="height:160px;">
        <?php foreach($pokemon->getMove() as $key => $move): ?>
            <div class="move-detail-content" id="<?=$modal['id']?>_<?=get_class($move['class'])?>-content">
                <h6><?=$move['class']->getName()?></h6>
                <hr>
                <p><?=$move['class']->getDescription()?></p>
            </div>
        <?php endforeach; ?>
        <?php # 覚えようとしている技 ?>
        <div class="move-detail-content active" id="<?=$modal['id']?>_<?=get_class($modal['new_move'])?>-content">
            <h6><?=$modal['new_move']->getName()?></h6>
            <hr>
            <p><?=$modal['new_move']->getDescription()?></p>
        </div>
    </div>
</div>

 

IDの重複回避

次にIDの重複回避です。モーダルは複数呼び出されることがあるため、IDを使用する場合は重複回避対策が必須となります。

ボタン等でIDを使用していたため、こちらをクラスに置き換えています。

<div class="modal-footer">
    <?php # 忘れさせるボタン ?>
    <button type="button"
    class="btn btn-danger btn-sm btn-forget-move"
    data-modal="#<?=$modal['id']?>-modal"
    data-msg_id="<?=$modal['id']?>"
    style="display:none;">
    <span class="move-name"></span>を忘れる</button>
    <?php # 諦めるボタン ?>
    <button type="button"
    class="btn btn-secondary btn-sm action-message-box btn-abandon-move"
    data-dismiss="modal">
    <?=$modal['new_move']->getName()?>を諦める</button>
</div>

 

また、jsで対象を指定する際に別のモーダルの値を取ってきてしまわないように、クリック発火するエレメントにはdata属性のmodalに現在のモーダルの判別値を格納しています。

 

これに合わせて、js側も修正しましょう。

  

バトル画面用js/Public/Assets/js/Battle/fight.js
/*----------------------------------------------------------
// 初期化する関数
----------------------------------------------------------*/
 
/**
* 技テーブルクリック時の関数
* @function click
* @return void
**/
var clickMoveInit = function(){
    $('.move-table-row').on('click', function(){
        // 技をフォームへセット
        $('#fight-form-param').val($(this).data('key'));
        // サブミット実行
        $('#fight-form').submit();
    });
}
 
/**
* 技テーブルクリック時の関数
* @function click
* @return void
**/
var selectForgetMoveInit = function(){
    $('.forget-selectmove').on('click', function(){
        var modal = $(this).data('modal');
        // 諦めるボタンの無効化切り替え
        if($(this).hasClass('new-move')){
            $(modal).find(".btn-abandon-move")
            .prop('disabled', false);
            $(modal).find('.btn-forget-move')
            .hide();
            return;
        }else{
            $(modal).find("#btn-abandon-move")
            .prop('disabled', true);
        }
        // 技名を取得
        var name = $(this).data('name');
        // ボタンに技名をセット
        $(modal).find('.btn-forget-move .move-name')
        .text(name);
        $(modal).find('.btn-forget-move')
        .show();
    });
}
 
/**
* 技テーブルクリック時の関数
* @function click
* @return void
**/
var submitForgetMoveInit = function(){
    $('.btn-forget-move').on('click', function(){
        var modal = $(this).data('modal');
        // 技をフォームへセット
        var data = {
            id: $(this).data('msg_id'),
            forget: $(modal).find('.forget-selectmove.active').data('num'),
            level: $('#level').text(),
            hp: $('#hpbar-friend').attr('aria-valuenow')
        };
        // フォームを用意
        $.each(data, function(key, val){
            // フォームの最初にパラメーターを追加
            $('#remote-form').append(
                '<input type="hidden" name="param[' + key + ']" value="' + val + '">'
            );
        })
        // サブミット実行
        $('#remote-form-action').val('learn_move');
        $('#remote-form').submit();
    });
}
 
/*----------------------------------------------------------
// 初期化
----------------------------------------------------------*/
jQuery(function($){
    clickMoveInit();
    selectForgetMoveInit();
    submitForgetMoveInit();
});

 

これで連続レベルアップと複数の技習得にも対応させることができました。実際に動きを見てみましょう。

 

 

1つのレベルに数個の技、レベルアップ時後にも技の習得を設定していましたが、問題なく動作してくれました。

これで技習得処理は完成です。

 

まとめ

いかがだったでしょうか。

今回のPHPポケモンではセッションを経由したオブジェクトの引き渡し方法をご紹介しました。

 

システム開発では方法を模索することがとても大切です。そして、複雑化しそうなときは1から考え直してみると、意外な突破口が見えてきます。

今回もオブジェクトをどう処理しようか、既にモーダルにセットした技を更新するにはどうすれば良いか、などを考えていましたが、考えれば考えるほど処理が複雑化しそうだったので、一度最初から考え直し、そもそも「すべての技を格納しておく必要がない」という答えに行き着きました

 

現在プログラミング学習に取り組んでいる方、ゲームづくりに興味がある方は、ぜひ参考にしてみてくださいね。

 

注目の記事

わざマシン編 忘れさせる技の選択 PHPポケモン106
プログラミング
PHP,PHPポケモン,ポケモン
わざマシン編 忘れさせる技の選択 PHPポケモン106

忘れさせる技の選択 わざマシンによる技習得処理を作成しましたが、既に覚えている技が4つあると、モーダルが表示されて選択をしても習得することができません。これは、技習得用のサービスがホーム画面には用意されていないからです。 なので今回は、わざマシンを使った際の技の入れ替え処理を実装していきましょう...

ポケモン預かりシステム編 ボックスの作成 PHPポケモン 86
プログラミング
PHP,PHPポケモン,ポケモン
ポケモン預かりシステム編 ボックスの作成 PHPポケモン 86

ポケモン預かりシステムの実装 今回は、前回ざっくりと仕様決めをした「ポケモン預かりシステム」を実装していきます。ボックス内では操作する項目が多いため、ボックス自体に1つの画面を用意して、できる限りPHPによる制御だけで完結できるように作成していきます。   ボックスクラスの作成 それではまず、今...

PHPポケモン「バトルシステム実装編〜補正値計算・乱数・急所〜」21
プログラミング
PHP,PHPポケモン,ポケモン
PHPポケモン「バトルシステム実装編〜補正値計算・乱数・急所〜」21

バトルシステムの実装  今回は「急所」と「乱数」と「タイプ一致」の判定と補正を実装していきます。 ちなみにですが、ポケモンwikiを熟読したところ、補正値の計算にも順番があり、計算後に小数点の切り捨てや五捨五超入をするなど、そこそこ複雑な計算順序がありましたが、今回はそこまで精密に再現せず、補正値(...

命中率補正と交代技編 PHPポケモン 45
プログラミング
PHP,PHPポケモン,ポケモン
命中率補正と交代技編 PHPポケモン 45

命中率補正とは ダメージ計算や行動順判定には補正(ランク)を計算した結果を反映していましたが、命中率補正についてはまだ未実装だったため、今回はこちらを作成していきたいと思います。 まずはwikiを参考に、計算方法を見ていきましょう。 命中(ポケモンwiki) https:// wiki.ポケモン.com/wiki/命中 第...

有料ノートが公開停止!?プラットフォーム依存の危険性とは
雑記
note,YouTube,ブログ
有料ノートが公開停止!?プラットフォーム依存の危険性とは

  「いままでのnoteがいきなり公開停止になった」 「YouTubeであげていた人気動画が削除された」   規約変更、違反などを理由にこういった被害は年々増えています。自覚して悪意あるコンテンツを作っているユーザーであれば当然の結果だと言ってしまえば済むことですが、努力して収益化する糸口を見つけ...

くれくれ姿勢が実は起業の近道だった!相手のことを考え過ぎるとハマる落とし穴とは
雑記
フリーランス,独立,起業
くれくれ姿勢が実は起業の近道だった!相手のことを考え過ぎるとハマる落とし穴とは

  仕事ください アドバイスしてください 〇〇について教えてください   こういったくれくれ姿勢の人は多く、投げかけられた側からすると「メリットは?」と感じてしまいます。 ですが実はこのくれくれ姿勢には意外な成功の要因が隠れています。多くの自己啓発記事やツイートをする人はこの本質的なものに触...

ピカチュウから学ぶオブジェクト指向 〜レベルシステム導入編〜 3
プログラミング
PHP,PHPポケモン,オブジェクト指向,ポケモン
ピカチュウから学ぶオブジェクト指向 〜レベルシステム導入編〜 3

  大人気?シリーズ「ピカチュウから学ぶオブジェクト指向」、今回はレベルシステムの導入編です。   第1回(基礎編)、第2回(クラス継承編)で作成したPokemonとPikachuのクラスを使用するので、最初から学習したい人はぜひご参考ください。      レベルシステムの導入   ポケモンのゲ...

トレーナー戦編 トレーナー情報の作成 PHPポケモン 97
プログラミング
PHP,PHPポケモン,ポケモン
トレーナー戦編 トレーナー情報の作成 PHPポケモン 97

トレーナー戦 いよいよPHPポケモンでもトレーナー戦の実装に取り掛かっていきます。バトルシステム自体は野生ポケモンと同じですが、トレーナーバトルでは以下の項目が追加、または制限を設けることになります。 複数匹のポケモン 逃げられない 捕まえられない 賞金   複数匹のポケモン ざっくり...

カテゴリ

SEO対策 イベント デザイン ネットワーク ビジネスモデル フリーランス プログラミング マーケティング ライティング 動画編集 雑記

タグ

5G Adobe AfterEffects AI ajax amazon Animate api artisan atom Automator AWS Bluetooth CSS CVR description EC-CUBE4 ECショップ ESLint Facebook feedly foreach function Google Google AdSense Honeycode htaccess HTML IEEE 802.11ax Illustrator Instagram IoT JavaScript jQuery jQuery UI keyword LAN Laravel Linux MacBook MAMP meta MLM MySQL NoCode note OS OSI参照モデル Paypal Photoshop PHP phpMyAdmin PHPポケモン PremierePro rss SEO SEO対策 Sequel Pro Skype SNS SSH Symfony TCP/IP title Toastr Trait Twig Twitter UCC V系 WAN WebSub Wi-Fi wiki Windows WordPress XAMPP xml Xserver YouTube YouTuber Zoom アーティスト アウトプット アクセス層 アニメーション アフィリエイト イーブイ インターネット インプット エンジニア オブジェクト指向 お金配り クリック単価 クリック数 コミュニケーション能力 コロナ コンサルティング サムネイル システムエンジニア スタートアップ スタイルシート スパム データベース ディープフェイク デザイナー デザイン テレワーク ナンパ ニュース ネットワークモデル ノマドワーク バナー ピカチュウ ビジネス フィード フリーランス ブロガー ブログ プログラマー プログラミング プログラミング学習 プログラミング教育 プロトコル ホームページ制作 ポケモン マークアップ マーケティング メール リモートワーク レンダリング 三井住友 三宮 仕事依頼 児童デイ 児童デイサービス 児童発達支援 公開鍵 初心者 助成金 勉強法 営業 広告 広告収入 必勝マニュアル 放課後等デイサービス 朝活 楽天 深層学習 無線LAN 独立 神戸 福祉 秘密鍵 翻訳 自己啓発 英語 見積書 計算機 読書 起業 迷惑メール 配列 銀の弾丸 集客 雑学力