まるぼ実験場

アプリの開発日記を載せるサイトでしたが、ただの技術ブログになりました。

ウィルコムPHS用のゲームを自作した記録(3)

最初にサンプルを動作させてみる。

さて、開発の必須ツールであるj2meを起動するとこんな画面が表示されます。
f:id:kikyou_kiki:20180505211057p:plain
メニューとコンソールだけのシンプルな画面構成。コンソール部にエラー内容が出力されます。
では、プロジェクトを作成していきます

新規プロジェクトを作成

新規プロジェクト作成を押します。
f:id:kikyou_kiki:20180505211201p:plain
クラス名とクラス名を入力します。
f:id:kikyou_kiki:20180505211237p:plain
API設定では、
marubodiary.hateblo.jp
の項で確認した項目を設定していきます。
402KCの場合は、JTWI+CLDC1.1を選択し了解します。作成するプログラムや動作させる機種に合わせて他は設定します。(今回は特に弄らなくても実機動作します)
ここの設定を間違えると、パッケージされたファイルが実機でのインストール時に弾かれたりする現象が発生したりします。

作業フォルダをエクスプローラでさがす
f:id:kikyou_kiki:20180505212208p:plain
C\Users\(アカウントのユーザー名)\j2mewtk\2.5.2\apps\(プロジェクト名)に作業フォルダが作られるようです。
ここのフォルダsrc内にファイルを作成し編集していきます。
ここでは、公式サイトからダウンロードしたサンプルFontSample.javaを動作させます。
FontSample.javaとFontSampleCanvas.javaを作業フォルダにコピーし、エディタで編集します。*1
たとえば、クラス名がtestの場合このように変更。

FontSample.java 4~13行目

/** FontSample*/
public class test extends MIDlet { //ここのFontSampleを新規プロジェクト作成時設定したクラス名に変更
	static MIDlet midlet;

    /** コンストラクタ */
    public test() { //ここも同じく
		midlet =this;
        Display.getDisplay(this).setCurrent(new FontSampleCanvas());
    }

FontSampleCanvas.java 60~65行目

	public void commandAction(Command c, Displayable d) 
	{
		if (c == exit){
			test.midlet.notifyDestroyed(); //ここも同じく
		}
	}

編集が終わったら上書き保存してj2meに戻り、ビルド→エラーがなければ実行ボタンを押します。
f:id:kikyou_kiki:20180505215048p:plainf:id:kikyou_kiki:20180505215054p:plain
決定ボタンで(プロジェクト名)を選ぶと実行できます。

パッケージの作成

実機で動作させるためのパッケージを生成します。
f:id:kikyou_kiki:20180505220015p:plain
パッケージ化に成功したら、先ほどの作業フォルダ下、\bin内にjarとjadが生成されます。
f:id:kikyou_kiki:20180505220513p:plain
このjarかjadのどちらかを携帯端末に転送します。

Webページから転送する方法もありますが、今回はSDカードを使用します。
携帯上でフォーマットしたSDカードの、
(SDカードのドライブ):\PRIVATE\KYOCERA\DATA(402KCの場合。機種によって差異があるかも)
にjarかjadをコピーします。(USBマスストレージでも可)
f:id:kikyou_kiki:20180505222320j:plain
データフォルダ→microSDデータからコピーしたファイルを探す。
f:id:kikyou_kiki:20180505222331j:plainf:id:kikyou_kiki:20180505222348j:plain
インストールして起動する

プロジェクトの読み込み

j2meを起動し、プロジェクトを開く、を押すとこのような画面が出ます。
f:id:kikyou_kiki:20180506235358p:plain
プロジェクト名を選択し、プロジェクトを開く。
作業フォルダのファイルを開き、編集を再開します。

エミュや実機上で無事に新規プログラムを動作させる手順をつかめたら、あとはソースの編集→ビルド(エラーが出たらデバッグ)を繰り返してゲームを作ります。

ゲームに不可欠なグラフィックの利用方法をまとめます。

Canvasへの描画

サンプル、GraphicsSampleCanvas.javaが参考になります。
paintメソッド内
色指定方法

        g.setColor(255,255,255); //RGB指定

ラインの描画

        g.drawLine(10,30,50,30); //原点x,y、終点x,y

        //破線の描画
        g.setStrokeStyle(Graphics.DOTTED); //スタイルに破線を設定
        g.drawLine(60,30,100,30);                    //このdrawLineは破線が描画される
        g.setStrokeStyle(Graphics.SOLID);       //スタイルを戻す

四角形の描画

        g.drawRect(10,60,40,40);    //原点x,y、幅、高さ
        g.fillRect(60,60,40,40);        //四角形の塗り潰し

        g.fillRect(0,0,getWidth(),getHeight());        //背景の塗り潰し
        //getWidthとgetHeightで画面サイズを取得できるので、そのサイズで塗りつぶしている

        //角丸四角形の描画
        g.drawRoundRect(10,110,40,40,20,20); // 原点x,y、幅、高さ、弧の幅、弧の高さ
        g.fillRoundRect(60,110,40,40,20,20);        //角丸四角形の塗り潰し

円弧の描画

        g.drawArc(10,160,40,40,0,270); // 原点x,y、直径x,y、開始角度、弧の角度
        g.fillArc(60,160,40,40,0,270);        //円弧の塗り潰し
        // 角度の単位は度(degree)

三角形の描画

        //三角形の塗り潰し
        g.fillTriangle(10,250,35,210,60,250); //三角形の頂点をx,y3つ指定

javaアプレットのpaintを使用したことがあれば、同じような感覚で使用できるでしょう。

画像の利用

f:id:kikyou_kiki:20180504203610p:plain
画像を表示させるだけならばImageメソッドで十分可能ですが、ここではSpriteメソッドを使用して、画像を表示させていきます。
SpriteにはImageにはない便利な機能があるので、活用することによって画像容量&ファイル数削減に役立てることができます。
グラフィックや音楽などのリソースはプロジェクトフォルダのresフォルダに保存すること。

読み込み

Image im_title;
im_title = Image.createImage("/teruri.png");
Sprite title = new Sprite(im_title, 250, 250);

画像のフレーム指定

title.setFrame(0); //x=0-250,y=0-250の範囲をセット
title.setFrame(1); //x=250-500,y=0-250の範囲をセット
title.paint(g);       //(1)のみ描画される
title.setFrame(0); //x=0-250,y=0-250の範囲をセット
title.paint(g);       //(0)描画
title.setFrame(1); //x=250-500,y=0-250の範囲をセット
title.paint(g);       //(1)描画

Imageクラスで読み込んだ画像をSpriteに指定します。Spriteでは反転やフレームを指定できるのでアニメーションに便利。
一つの画像にまとめることによって読み込み回数を減らすことができ、その点でも有利(自分はそうしてないけれども
この機能を使用して、タイトル画面を表示させています。
 f:id:kikyou_kiki:20180504203501p:plain
タイトル画面は差分となっていて、順次重ねていくことによって変化をつけています。
f:id:kikyou_kiki:20180518002139p:plain
例えば、クリア1回目はこうなる 

画像の反転

f:id:kikyou_kiki:20180504203539p:plain
プレイヤーはこの画像を半分に分割したものを反転を利用して表示しています。

drawGamePlayerクラス

if(player.getdir() == 1){
	p.setTransform(2);
	p.setFrame(1);
}else if(player.getdir() == -1){
	p.setTransform(0);
	p.setFrame(1);
}else{
	p.setFrame(0);
}
/*
setTransform(0):そのままの画像。TRANS_NONE
setTransform(2):左右反転した画像。TRANS_MIRROR
*/

プレイヤーが移動している場合、移動方向を取得し、frame(1)を移動方向に応じて反転したものをセットしています。
詳しくは、リファレンスのSpriteの項に色々載ってます:Sprite (Unofficial 'CLDC 1.1 + MIDP 2.0' API Reference.)

音楽ファイルの使用方法について解説します

基本となりそうな部分について。

変数宣言

Player player=null;//プレイヤーの作成
String ss=null; //ファイル名

Player型変数playerを宣言

サウンドファイルの読み込み

try {
	InputStream in=getClass().getResourceAsStream(ss);
	player=Manager.createPlayer(in,"audio/midi");
}catch (Exception e) {
	e.printStackTrace();
}

再生

try{  
	player.start();
}catch (Exception e) {
	e.printStackTrace();
}

InputStreamでファイルを入力。
Manager.createPlayerでplayerにファイルを渡す。
player.start()やplayer.stop()で再生停止ができます。

SEの使用について 補足

f:id:kikyou_kiki:20180504165010p:plain
端末上でSEを使用するには、wav(量子化ビット数2/4bit サンプリング周波数8/16/32kHz)に変換する必要があるそうなのですが、自分が見つけた変換ソフトのほとんどがWindows10に対応していない模様?よくXP環境で挙げられているサウンドレコーダーも見つからない。
とりあえず仕方ないので現状のwavファイルのままで完成ビルドにいれています。WX402KCではSEが再生されないという状況ですが、もし他の機種で再生されたなどありましたら、情報提供していただけると幸いです。

*1:プロジェクト新規作成時、クラス名をFontSampleにしていた場合この作業は必要ない

ウィルコムPHS用のゲームを自作した記録(2)

どんなゲームにするか。

ゲーム内容(コンセプト)を一言でいうと、ワンボタンで遊べる避けゲー。
ざっくり言うと、シューティングゲームから弾を避ける要素のみにしたような感じ。
ワンボタンで遊べるという手軽さから、ガラケーでの操作にも似合い、制限された環境でも動作させられるという点がポイント。
個人的に開発したことのないプラットフォームでの開発を経験してみたいと感じ、敢えてガラケーJavaというプラットフォームをチョイスした。

ゲーム仕様

ボタンを押している間キャラクターは右に移動。離すと左に戻っていく。
オブジェクトはランダムに降ってくる。
オブジェクトにキャラクターが触れるとライフが減少する。
UIとして、進行状況を表すバーとスコア表示を常に画面に表示
スコアが上昇している間には表示とSEの再生を入れる。
避けるだけのゲーム内容で単調だったので何かしら要素を追加 →降ってくるオブジェクトに接近することで得点が増加するように変更。ギリギリのスリルが味わえます(?)*1
ストーリー面 →クリアごとにタイトル画面が変わる
ゲーム内容自体はこんなシンプルなルールしかありません。元々初めて作ったアクション系ゲームなので面白いかどうかはともかくプログラムとしての想像が沸きやすい、といった点を重視した所このようなゲームに。

メイン関数内の解説。

run()を実行してwhileに入るまでの長々とした部分でゲーム内の変数を生成しています。
whileに入ってからはswitch文で分岐させています。遷移を図にしてみるとこのようになっています。
f:id:kikyou_kiki:20180504153801p:plain
図にしてみるともっとわかりやすい分岐ありそうだと思うんですよね…ゲームはまだ見よう見まねで作ってるので詳しい人教えてください!
 コードを見てみると、switch文の各caseに処理と描画メソッドの呼び出しが入っており、処理に続いて描画メソッドの呼び出しが入っています。
メモリが限られた環境でパフォーマンスを意識する場合はあまり関数分けしない方が良いと聞いたので*2、描画部分や長くなっている部分以外はそのままにしたため、このようになっています。

ソースコード

呼び出し部分。Y!mobileのサンプルを参考に。

import javax.microedition.lcdui.*;
import javax.microedition.midlet.*;

public class Teruteru extends MIDlet {
	static MIDlet midlet;

    //コンストラクタ
    public Teruteru() {
		midlet=this;
        TeruteruCanvas sc = new TeruteruCanvas();
        Display.getDisplay(this).setCurrent(sc);
        Thread thread=new Thread(sc);
        thread.start();
    }

    //アプリの開始
    public void startApp() {
    }

    //アプリの一時停止
    public void pauseApp() {
    }

    //アプリの終了
    public void destroyApp(boolean flag) {
    }
}

メインの部分。

import java.util.Random;
import javax.microedition.lcdui.*;
import javax.microedition.midlet.*;
import javax.microedition.lcdui.game.*;

import javax.microedition.media.*;
import javax.microedition.media.control.*;
import java.io.*;

public class TeruteruCanvas extends GameCanvas implements Runnable,CommandListener{
	//画像
	Image im_title;
	Image im_Player;
	Image im_ame;
	Image im_bg;
	Image im_gover;
	//BGM
	Player sndBGM;
	Player goverBGM;
	Player damageSE;
	Player scoreSE;
	
	String BGMname;
	String SEname;
	
	//ディスプレイサイズ取得
	int display_width = getWidth();
	int display_height = getHeight();
	
	//ランダム生成
	Random random = new Random();
	//ソフトキー
	Command exit;
	Command inst;
	
	String keyCommand = ""; //コマンド保存
	
	boolean inst_flag = false;
	boolean flag;
	
	//コンストラクタ
	TeruteruCanvas(){
		//キーイベントの抑制
		super(false);
		
		//画像の読み込み
		try{
			im_title = Image.createImage("/teruri-1.png");
			im_Player = Image.createImage("/player_sp.png");
			im_ame = Image.createImage("/ame.png");
			im_bg = Image.createImage("/bg.png");
			im_gover = Image.createImage("/gameover.png");
			
		}catch(Exception e){
			System.out.println(e.getClass().getName());
		}
		//BGMの読み込み
		try {
			InputStream in_bgm = getClass().getResourceAsStream("/teruteru_pops.mid");
			InputStream in_gover = getClass().getResourceAsStream("/teruteru_Healing_Harp.mid");
			InputStream in_damage = getClass().getResourceAsStream("/se_damage.wav");
			InputStream in_score = getClass().getResourceAsStream("/se_score.wav");
			
			sndBGM = Manager.createPlayer(in_bgm, "audio/midi");
			goverBGM = Manager.createPlayer(in_gover, "audio/midi");
			damageSE = Manager.createPlayer(in_damage, "audio/x-wav");
			scoreSE = Manager.createPlayer(in_score, "audio/x-wav");
			sndBGM.setLoopCount(-1);
		}catch (Exception e) {
			e.printStackTrace();
		}
		
		//ソフトキーの設定
		exit = new Command("終了", Command.EXIT, 0);
		addCommand(exit);
		inst = new Command("操作説明", Command.SCREEN, 1);
		addCommand(inst);
		setCommandListener(this);
	}
	
	//雨粒オブジェクトクラス
        //落ちてくるオブジェクトの座標の生成、移動、生存の情報を持っているクラス。オブジェクトを生成するとランダムで座標を決定します。
	class Object_Ame {
		//雨粒初期位置
		private int x;
		private int y;
		//雨粒画像サイズ 現状は直入力
		private int ame_width;
		private int ame_height;
		//雨粒移動距離
		private int dy;
		//画面内生存判定
		private boolean suv;
		
		private int rnd;
		
		//コンストラクタ
		Object_Ame(){
			Set();
			this.ame_width = 32;
			this.ame_height = 5;
			this.dy = 5;
			this.suv = true;
		}
		
		int getX(){return x;}
		int getY(){return y;}
		int getWidth(){return ame_width;}
		int getHeight(){return ame_height;}
		boolean getState(){return suv;}
		
		//xy軸決定
		void Set(){
			setX(randX());
			setY(randY());
		}
		void setX(int setx){
			x = setx;
		}
		void setY(int sety){
			y = sety;
		}
		
		//ランダム
		int randX(){
			int field_width = display_width/10;
			rnd = random.nextInt(display_width) - field_width - ame_width*2;
			if(rnd < 0) randX();
			return rnd;
		}
		int randY(){
			int field_height = display_height;
			rnd = (random.nextInt(display_height) - field_height) + 50;
			//if(rnd < 0) randY();
			return rnd;
		}
		
		//dy方向移動
		void MoveAme(){
			y += dy;
		}
		
		//死亡判定
		void deadState(){
			suv = false;
		}
	}
	//プレイヤーに関する情報と制御を行うクラス。inv_checkとisinvの役割がかぶってる気がしますが、inv_checkはインクリメントしてるのでコード中1回はこっちを呼び出すんだと思う()多分もっとうまくやれる
	class Object_Player {
		private int life;
		private int p_width; //プレイヤースプライトサイズ
		//プレイヤー初期位置
		private int x;
		private int y;
		//移動速度
		private int dir; //移動方向
		private int dx; //通常移動速度
		//無敵時間
		private int inv; //無敵時間
		private int inv_f; //無敵経過時間
		//接近フラグ
		private boolean close;
		//点滅管理用
		private int visflag;
		
		Object_Player(){
			this.life = 4;
			this.p_width = 40;
			this.x = 0;
			this.y = display_height * 4/ 5;
			this.dir = 0; //移動方向
			this.dx = 4; //通常移動速度
			this.inv = 25; //無敵時間
			this.inv_f = 0; //無敵経過時間
			this.visflag = 0;
			this.close = false;
		}
		int getX(){return x;}
		int getY(){return y;}
		int getdir(){return dir;}
		int getinv(){return inv;}
		int getinv_f(){return inv_f;}
		int getlife(){return life;}
		int getvisflag(){return visflag;}
		boolean getclose(){return close;}
		
		void PlayerControl(int key){
			//ボタン進行判定
			if (key != 0){
				setdir(1);
			}else if(getX() > 0){
				setdir(-1);
			}else{
				setdir(0);
			}
			PlayerMove();
		}
		
		void PlayerMove(){
			x += dx * dir;
		}
		
		void setdir(int setDir){
			dir = setDir;
		}
		void setinv_f(int sett){
			inv_f = sett;
		}
		
		void inv_check(){
			if(inv_f >= inv){
				setinv_f(0);
			}else if(isinv()){
				inv_f++;
			}
		}
		boolean isinv(){
			if(inv_f >= 1 & inv_f < inv){
				return true;
			}
			return false;
		}
		
		void lifedown(){
			life--;
		}
		
		int switchvisflag(){
			if(visflag == 0){
				visflag = 1;
			}else if(visflag == 1){
				visflag = 0;
			}
			return visflag;
		}
		
		void setclose(boolean st){
			close = st;
		}
		
		void spacehit(){
			int field_width = display_width/10;
			if(x < 0) x = 0;
			if(x > display_width - p_width - field_width) x = display_width - p_width - field_width;
		}
	}
	// ステージ制御
	class Stage {
		private int stagesize; //ステージ長さ
		private int progress; //進行状況
		private int score; //スコア
		//画面内の雨粒数
		private int ame_st;
		private int ame_max;
		private int framecount; //フレームカウント
		
		Stage(){
			this.stagesize = 1000; //ステージ長さ
			this.progress = 0; //進行状況
			this.score = 0; //スコア
			//画面内の雨粒数
			this.ame_st = 7;
			this.ame_max = 7;
			this.framecount = 0;
		}
		int getProgress(){return progress;}
		int getStage(){return stagesize;}
		int getScore(){return score;}
		int getAmest(){return ame_st;}
		int getAmemax(){return ame_max;}
		int getFramecount(){return framecount;}
		
		void incProgress(){
			progress++;
		}
		
		void incScore(){
			score++;
		}
		
		boolean StageEnd(){
			if(progress > stagesize){
				return true;
			}
			return false;
		}
		
		void count_up(){
			framecount++;
		}
	}
	// あたり判定などの制御
	class Sqer {
		private int x;
		private int y;
		private int w;
		private int h;
		
		Sqer(int sq_x, int sq_y, int sq_w, int sq_h){
			this.x = sq_x;
			this.y = sq_y;
			this.w = sq_w;
			this.h = sq_h;
		}
		
		int getX(){return x;}
		int getY(){return y;}
		int getW(){return w;}
		int getH(){return h;}
		int getCX(){
			return (x+w)/2;
		}
		int getCY(){
			return (y+h)/2;
		}
	}
	
	public void run(){
		Graphics g = getGraphics();
		int scene = 1; //シーン情報
		
		//ゲームグラフィック
		Sprite title = new Sprite(im_title, 250, 250);
		Sprite p = new Sprite(im_Player, 50, 50);
		Sqer p_cl = new Sqer(16, 4, 18, 19);
		p.defineCollisionRectangle(p_cl.getX(), p_cl.getY(), p_cl.getW(), p_cl.getH());
		Sprite bg = new Sprite(im_bg);
		Sprite gover = new Sprite(im_gover);
		//ステージ生成
		Stage s = new Stage();
		//配列変数
		Object_Ame[] ame = new Object_Ame[s.getAmemax()];
		Sprite[] am = new Sprite[s.getAmemax()];
		Sqer am_cl = new Sqer(2,11,12,12);
		for(int i = 0; i < s.getAmemax(); i++){
			am[i] = new Sprite(im_ame);
			am[i].defineCollisionRectangle(am_cl.getX(), am_cl.getY(), am_cl.getW(), am_cl.getH());
		}
		//プレイヤー生成
		Object_Player player = new Object_Player();
		//ゲージ用変数設定
		Sqer g1 = new Sqer(display_width - display_width/10, display_height/10, display_width/25, (display_width * 3)/5);
		
		//Font
		Font largefont;
		Font font;
		largefont = Font.getFont(Font.FACE_MONOSPACE, Font.STYLE_PLAIN,Font.SIZE_LARGE);
		font = Font.getFont(Font.FACE_MONOSPACE, Font.STYLE_PLAIN,Font.SIZE_MEDIUM);
		
		int clear = 0; //クリア回数
		int high_score = 0; //ハイスコア
		
		int keyState = 0; //キー情報
		flag = true;
		
		while(flag){
			//画面の初期化
			g.setColor(255,255,255);
			g.fillRect(0,0,display_width,display_height);
			
			//ボタン操作取得
			keyState = getKeyStates();
			
			switch(scene){
			case 0: //init
				player = new Object_Player();
				s = new Stage();
				//配列初期化
				ame = new Object_Ame[s.getAmemax()];
				//コマンド初期化
				keyCommand = "";
				
				scene = 1;
				break;
			case 1: //タイトル画面
				//スタートキーが押された
				if((FIRE_PRESSED &keyState) != 0){
					scene = 2;
					//BGMスタート
					try{
						sndBGM.start();
						sndBGM.setMediaTime(0); //最初から再生します
					}catch (Exception e) {
						e.printStackTrace();
					}
				}
				
				//操作説明キーが押された
				if(inst_flag == true){
					scene = 5;
				}
				
				drawTitle(g, font, largefont, title, high_score, clear);
				
				break;
			case 2: //ゲームループ
				
				//ボタン進行判定
				player.PlayerControl(FIRE_PRESSED &keyState);
				//移動制限
				player.spacehit();
				//当たり判定
				GameisHit(ame, am, player, p, s, am_cl, p_cl, s.getAmest());
				//無敵継続処理
				player.inv_check();
				//進行増加
				s.incProgress();
				s.count_up();
				
				//終了判定
				if(s.StageEnd()){
					scene = 4;
					try{  
						sndBGM.stop();
					}catch (Exception e) {
						e.printStackTrace();
					}
				}else if(player.getlife() <= 0){
					scene = 3;
					try{  
						sndBGM.stop();
					}catch (Exception e) {
						e.printStackTrace();
					}
				}
				
				//隠しコマンド
				if(keyCommand.equals("6109")){
					player.setinv_f(1);
				}else if(keyCommand.equals("37564")){
					player.lifedown();
				}
				
				//描画
				drawGameBackground(g, bg);
				drawGamePlayer(g, player, p, s.getFramecount());
				drawGameMoveObj(g, ame, am, s.getAmest());
				drawGameUI(g, font, largefont, s.getScore(), player.getlife(), player.getclose(), s, g1);
				
				break;
			case 3: //ゲームオーバー画面
				//BGM
				try{
					goverBGM.start();
				}catch (Exception e) {
					e.printStackTrace();
				}
				
				//スコアを更新
				if(s.getScore() > high_score){
					high_score = s.getScore();
				}
				
				//描画
				drawGameOver(g, font, gover, s.getScore());
				
				try {
				Thread.sleep(500);
				} catch (Exception e){
				}
				
				if((FIRE_PRESSED &keyState) != 0){
					scene = 0;
					try{
						goverBGM.stop();
						goverBGM.setMediaTime(0);
					}catch (Exception e) {
						e.printStackTrace();
					}
				}
				
				break;
			case 4: //クリア画面
				//スコアを更新
				if(s.getScore() > high_score){
					high_score = s.getScore();
				}
				//描画
				drawClearStage(g, font, s.getScore(), player.getlife(), bg);
				
				try {
				Thread.sleep(1000);
				} catch (Exception e){
				}
				
				if((FIRE_PRESSED &keyState) != 0){
					scene = 0;
					clear++;
				}
				
				break;
			case 5: //操作説明
				if(inst_flag == false){
					scene = 1;
				}
				//描画
				Instraction(g, font);
				
				break;
			}
			//画面に反映
			flushGraphics();
			
			//スリープ
			try {
				Thread.sleep(100);
			} catch (Exception e){
			}
		}
		
		//アプリ終了通知
		Teruteru.midlet.notifyDestroyed();
	}
	
	public void keyPressed(int keyCode) {
		if (keyCode==0){
			return;
		}
		
		switch(keyCode) {
			//数字キー・記号キー
			case KEY_NUM0: keyCommand+="0";break;
			case KEY_NUM1: keyCommand+="1";break;
			case KEY_NUM2: keyCommand+="2";break;
			case KEY_NUM3: keyCommand+="3";break;
			case KEY_NUM4: keyCommand+="4";break;
			case KEY_NUM5: keyCommand+="5";break;
			case KEY_NUM6: keyCommand+="6";break;
			case KEY_NUM7: keyCommand+="7";break;
			case KEY_NUM8: keyCommand+="8";break;
			case KEY_NUM9: keyCommand+="9";break;
			case KEY_STAR: keyCommand ="";break;
		}
	}
	public void GameisHit(Object_Ame[] ame, Sprite[] am, Object_Player player, Sprite p, Stage s, Sqer am_cl, Sqer p_cl, int ame_st){
		//接近変数
		int px2=0;
		int px1=0;
		int py2=0;
		int py1=0;
		int r = 40;
		int close_count = 0;
		//オブジェクト処理
		for(int i = 0; i < ame_st; i++){
			//オブジェクト生成
			if(ame[i] == null || ame[i].getState() == false){
				ame[i] = new Object_Ame();
			}else{
				//オブジェクト移動
				ame[i].MoveAme();
			}
			//範囲外に出たときの処置
			if(ame[i].getY() > display_height){
				ame[i].deadState();
			}
			//衝突判定
			if(player.getinv_f() == 0){ //無敵でないとき
				if(ame[i].getState() & p.collidesWith(am[i], true)){ //AmeがDeadでない&衝突している
					//SE再生
					try{
						damageSE.start();
						damageSE.setMediaTime(0); //最初から再生します
					}catch (Exception e) {
						e.printStackTrace();
					}
					player.lifedown();
					ame[i].deadState();
					player.setinv_f(1);
					break;
				}
				//かすり判定
				px2 = am[i].getX() + am_cl.getCX() -r/2;
				py2 = am[i].getY() + am_cl.getCY() -r/2;
				px1 = player.getX() + p_cl.getCX() -r/3;
				py1 = player.getY() + p_cl.getCY() -r/2;
				
				if(Math.abs(px2 - px1) * Math.abs(px2 - px1) + Math.abs(py2 - py1) * Math.abs(py2 - py1) < r*r){
					//接近スコア加算
					try{
						scoreSE.start();
					}catch (Exception e) {
						e.printStackTrace();
					}
					close_count++; //接近カウント
					s.incScore();
				}
			}
		}
		
		//1つ以上と接近でフラグON
		if(close_count > 0){
			player.setclose(true);
		}else{
			player.setclose(false);
		}
	}
	
	public void drawGameBackground(Graphics g, Sprite bg){
		//背景
		bg.setFrame(0);
		bg.setPosition(0, 0);
		bg.paint(g);
	}
	public void drawGamePlayer(Graphics g, Object_Player player, Sprite p, int framecount){
		//プレイヤーキャラ
		if(player.getdir() == 1){
			p.setTransform(2);
			p.setFrame(1);
		}else if(player.getdir() == -1){
			p.setTransform(0);
			p.setFrame(1);
		}else{
			p.setFrame(0);
		}
		
		//点滅演出
		if(player.isinv()){
			if(player.switchvisflag()==0){
				p.setVisible(false);
			}else{
				p.setVisible(true);
			}
		}else{
			p.setVisible(true);
		}
		
		//プレイヤー描画
		int p_x = player.getX();
		int p_y = player.getY();
		p.setPosition(p_x, p_y);
		p.paint(g);
	}
	public void drawGameMoveObj(Graphics g, Object_Ame[] ame, Sprite[] am, int ame_st){
		//障害物オブジェクト
		for(int i = 0; i < ame_st; i++){
			int am_x = ame[i].getX();
			int am_y = ame[i].getY();
			am[i].setPosition(am_x, am_y);
			am[i].paint(g);
		}
	}
	
	public void drawGameUI(Graphics g, Font font, Font largefont, int score, int life, boolean close, Stage s, Sqer g1){
		//進行ゲージ描画
		int x1 = g1.getX();
		int y1 = g1.getY();
		int w1 = g1.getW();
		int h1 = g1.getH();
		int progress = s.getProgress();
		int stage = s.getStage();
		g.setColor(0,0,0); //影
		g.drawRect(x1, y1, w1, h1);
		g.setColor(255,0,0); //実ゲージ
		g.fillRect(x1, y1, w1, h1);
		int gh_f = (int)((double)(1-((double) progress / stage)) * 100 * ((double)h1 / 100));
		g.setColor(0,128,128); //減る部分
		g.fillRect(x1 + 1, y1 + 1, w1 - 2, gh_f - 2);
		
		//スコア表示描画
		g.setColor(0,0,0);
		g.setFont(largefont);
		g.drawString("ENERGY:" + score, 0, 0, g.LEFT | g.TOP);
		
		//接近演出
		if(close){
			g.setColor(255,0,0);
			g.drawString("+ENERGY", 70, (s.getFramecount() % 3), g.LEFT | g.TOP);
		}
		
		//ライフ表示描画
		g.setColor(255,0,0);
		g.setFont(font);
		for(int i = 0; i < life; i++){
			g.drawString("●", 0 + (i * 14), 25, g.LEFT | g.TOP);
		}
	}
	
	public void drawTitle(Graphics g, Font font, Font largefont, Sprite title, int high_score, int clear){
		//描画
		title.setFrame(0);
		title.setPosition(0, 0);
		title.paint(g);
		
		title.setPosition(0, 0);
		if(clear >= 3){
			title.setFrame(3);
			title.paint(g);
		}else if(clear >= 2){
			title.setFrame(1);
			title.paint(g);
			title.setFrame(2);
			title.paint(g);
		}else if(clear >= 1){
			title.setFrame(1);
			title.paint(g);
		}
		
		g.setColor(0,0,0);
		g.setFont(largefont);
		g.drawString("HI SCORE:" + high_score, 0, 0, g.LEFT | g.TOP);
		g.drawString("PRESS START", display_width/3, display_height*4/5, g.LEFT | g.BOTTOM);
		g.setFont(font);
		g.drawString("★"+keyCommand, 0, display_height - 35, g.LEFT | g.TOP);
		g.drawString("(c) sui-kiki", 0, display_height, g.LEFT | g.BOTTOM);
	}
	
	public void drawGameOver(Graphics g, Font font, Sprite gover, int score){
		//描画
		g.setColor(0,0,0);
		g.fillRect(0,0,display_width,display_height);
		gover.setFrame(0);
		gover.setPosition(0, 0);
		gover.paint(g);
		
		g.setColor(255,255,255);
		g.setFont(font);
		g.drawString("Game Over", display_width/5, display_height*3/5+5, g.LEFT | g.TOP);
		g.drawString("Stage Score(ENERGY):" + score, display_width/5, display_height*3/5 + 20, g.LEFT | g.TOP);
		g.drawString("Restart PRESS BUTTON", display_width/3, display_height, g.LEFT | g.BOTTOM);
	}
	
	public void drawClearStage(Graphics g, Font font, int score, int life, Sprite bg){
		drawGameBackground(g, bg);
		g.setColor(255,0,0);
		g.setFont(font);
		g.drawString("Game Clear!!", display_width/4, display_height/2 - 10, g.LEFT | g.TOP);
		g.setColor(0,0,0);
		g.drawString("Stage Score(ENERGY):" + score, display_width/4, display_height/2 + 15, g.LEFT | g.TOP);
		g.drawString("Life Bonus(Life*50):" + life * 50, display_width/4, display_height/2 + 30, g.LEFT | g.TOP);
		g.drawString("Final Score:" + score + (life * 50), display_width/4, display_height/2 + 45, g.LEFT | g.TOP);
		g.drawString("Restart PRESS BUTTON", display_width/4, display_height*4/5, g.LEFT | g.TOP);
	}
	
	public void Instraction(Graphics g, Font font){
		//操作説明
		g.setFont(font);
		g.setColor(200,200,0);
		g.drawString("★操作方法★", 0, 0, g.LEFT | g.TOP);
		g.setColor(0,0,0);
		g.drawString("中央キーで右方向に移動し、", 0, 15, g.LEFT | g.TOP);
		g.drawString("離すと元の位置に戻っていきます。", 0, 30, g.LEFT | g.TOP);
		g.setColor(200,200,0);
		g.drawString("★得点の増やし方★", 0, display_height/5, g.LEFT | g.TOP);
		g.setColor(0,0,0);
		g.drawString("雨粒に近づくとエネルギーを吸収し、", 0, display_height/5+15, g.LEFT | g.TOP);
		g.drawString("スコアが増加します。", 0, display_height/5+30, g.LEFT | g.TOP);
		g.setColor(200,200,0);
		g.drawString("★隠しコマンド★", 0, display_height*2/5, g.LEFT | g.TOP);
		g.setColor(0,0,0);
		g.drawString("いろいろためそう", 0, display_height*2/5+15, g.LEFT | g.TOP);
		g.setColor(200,200,0);
		g.drawString("★クリア回数によって", 0, display_height*3/5, g.LEFT | g.TOP);
		g.drawString(" タイトルイラストが変化します★", 0, display_height*3/5+15, g.LEFT | g.TOP);
		g.setColor(200,200,0);
		g.drawString("★使用した素材★", 0, display_height*4/5 , g.LEFT | g.TOP);
		g.setColor(0,0,0);
		g.drawString("BGM:音楽研究所", 0, display_height*4/5+15 , g.LEFT | g.TOP);
		g.drawString("SE:TAM Music Factory", 0, display_height*4/5+30 , g.LEFT | g.TOP);
	}
	
	//キーリリースイベント
	public void keyReleased(int keyCode) {}
	
	public void commandAction(Command c, Displayable d) {
		if (c == exit) {
			flag=false;
		}else if(c == inst){
			//ボタン情報を送信
			inst_flag = !inst_flag;
		}
	}
}

関数一覧

case 1内

drawTitle(g, font, largefont, title, high_score, clear);
//タイトル画面描画メソッド タイトル画面で表示するフォント、スプライト、ハイスコア
//タイトル画面変更制御のフラグ(クリア回数)を入力

case 2内

player.PlayerControl(FIRE_PRESSED &keyState);
//ボタン進行判定
player.spacehit();
//移動制限
GameisHit(ame, am, player, p, s, am_cl, p_cl, s.getAmest());
//当たり判定 プレイヤーとオブジェクトの座標を入力
player.inv_check();
//無敵継続処理
s.incProgress();
//進行増加
s.count_up();
//カウンタ増加

GameisHit以外はクラスメソッドとなっている。

drawGameBackground(g, bg);
//背景描画
drawGamePlayer(g, player, p, s.getFramecount());
//プレイヤー描画 Framecountはダメージ受けた時の点滅描画のために使用。
drawGameMoveObj(g, ame, am, s.getAmest());
//オブジェクト描画 オブジェクト座標とスプライト情報とオブジェクト生存状況を入力
drawGameUI(g, font, largefont, s.getScore(), player.getlife(), player.getGraze(), s, g1);
//UI描画 フォント情報とスコア、ライフ、かすり、進行状況、四角の情報を入力

画像の位置関係の問題があるので上に表示したいものを最後に描画

case 3内

drawGameOver(g, font, gover, s.getScore());
//画面描画メソッド ゲームオーバー画面で表示するフォント、スプライト情報、スコア

case 4内

drawClearStage(g, font, s.getScore(), player.getlife(), bg);
//画面描画メソッド クリア画面で表示するフォント、スコア、ライフ、背景

case 5内

Instraction(g, font);
//draw抜けてるけど描画メソッド フォント情報

*1:シューティングゲームのシステムを参考にした。

*2:関数呼び出しにスタックを使用するのでそれがメモリに効いてくるとのこと。

ウィルコムPHS用のゲームを自作した記録(1)

※かなり昔に書いたブログ内容の移転です。正直今見返すのもアレなレベルなのですが、自らの記録のために書いておこうと思います。

学生時代に作った、Javaもゲームの作り方もよく分かっていなかった時期のプログラムですが、よくここまで調査して作ったなぁ……と思ったので。自分以外に今更ガラケー向きアプリを作成する奇特な人間のために(?)残しておきます。

ゲーム制作自体の解説より、どのようにして実機で動かしたかといったHowto的な側面をまとめることを主旨としたいと考えています。史料となる…かどうかは分かりませんが、制作の過程をまとめておきたいと感じ、執筆しました。

というか、もう新規契約が終了してしまったどころかサービス終了したプラットフォームのゲームを遊んでもらうことが絶望的であると悟ったので、ソースコードや色々を公開してしまおうと思ってこんなことを始めました。遅かったのは分かっているんです、でも何かの役に立てば幸いです。

制作成果物のダウンロード

京セラ製PHS、402KCでのみ実機動作確認。(製作途中一度だけWX05Kでも動かした)

402KC実機上ではSEが再生されないのは仕様となります。

SDカードのPRIVATE\DATAフォルダにjadを置くとデータフォルダに認識されるので、インストールを実行します。

jad(OneDriveなので表示に時間がかかるようです)

開発前の準備

まずは開発環境を整えました。

特有のものとしては、j2me Wireless Toolkit

Sun Java Wireless Toolkit 2.5.2 for CLDC Download

エミュレータが同梱されており、テストやソースコードからのビルドを行えるツール。現在ダウンロードにはOracleアカウントでのログインが必要。

 

このツールをインストールする前に、先にJava Development Kit(JDK)をインストールしておく必要があります。

Java SE - Downloads | Oracle Technology Network | Oracle

バージョンは当時の環境のことを思い出しながらあえて6にしました。

詰まった点として、JDKは32ビット版でないとJ2MEのインストール時に認識されません。64ビット版を既にインストールしている場合でも32ビット版が必要でした。

開発の参考にした資料(ほとんどWebのもの)

連載インデックス「携帯アプリを作って学ぶJava文法の基礎」 - @IT

初心者向け。一つのアプリを作っていく構成であるのでわかりやすい。

DoCoMo(Doja)とそれ以外のキャリアのMIDPの違いも解説されている。

 

www.ymobile.jp

各機種の仕様が載っている

 

www.ymobile.jp

サンプルコードなどがあるので参考になります。

ここの「サンプルコード」からダウンロードできるzip形式ファイルの中からゲーム制作で特に参考になったもの。

AudioPlaySample・・・音楽再生

FontSample・・・文字表示

GraphicsSample・・・Canvasの使用。塗りつぶしや図形描画

ImageSample・・・画像表示

KeyEventSample・・・キーイベント(数字キー記号キーの使用)

KeyEventSample2・・・キーイベント(方向キー決定キーの使用)

SpriteSample・・・Spriteの使用

www.ymobile.jp

詳細な機種スペックを参照

 

概要 (Unofficial 'CLDC 1.0 + MIDP 1.0' API Reference.)

Unofficial “CLDC 1.1 + MID Profile 2.0” API Reference. | KEI SAKAKI's PAGE.

MIDPの日本語リファレンス

 

モバイルの素-MIDP オープンアプリプレイヤー対応

 auオープンアプリプレイヤーでのアプリ作成

 

あとは自分の好きなテキスト編集ソフトを用意しておく。私はサクラエディタを使用しました。

今回は、ウィルコムY!mobilePHS、402KCで動作させることを目標としています。

www.ymobile.jp

最後の京セラ製PHSであり、PHS向けJavaアプリを動作させることのできる最後の端末。

OSは昔から変更なかったと思うので確かTRON。ちなみに今Y!mobileから出てるシャープ製のガラケー型端末はUIそっくり中身Androidの別物です。従来のJavaアプリは動きません。

 

さて、PHSにて採用されているJavaMIDPと呼ばれているもの。

ガラケーではSoftBankauオープンアプリEZアプリ(J))、EMOBILEで採用されていました(DoCoMoは言語はJavaの独自のもの)なので、各社独自の追加機能はあれどY!mobile(ウィルコム)はSoftBankauのアプリの作り方そのままでいけるようです。

PHSと携帯電話の違いについてはこの記事では割愛させていただきます。Javaアプリに関していえば性能や独自機能以外違いはないと思います。

また、細かな制限があるようです。

参考文献:http://www.atmarkit.co.jp/ait/articles/0809/17/news135_3.html

 現在、勝手アプリを制作したい場合ウィルコムau向け(契約している端末のみ?)ということになります

 

では、ターゲットの端末の性能を確認していきます

www.ymobile.jp

このページより、

>搭載するJavaVMは、MIDP 2.0/CLDC 1.1に準拠したもので、JSR-75のFile Connectionにも対応しております。
>また、一部の機種は位置情報、カメラのAPIマスコットカプセルV3.0にも対応しております。
>対応する画面解像度はQVGAで、描画可能領域(Canvas)は240x276です。

この辺はほぼ全機種共通と考えて良さそうです。

 

このページにあるJava機種情報と対応API一覧表は要チェック。

機種情報:https://www.ymobile.jp/service/contents_service/common/pdf/javatm_lineup.pdf

対応API一覧表:https://www.ymobile.jp/service/contents_service/common/pdf/javatm_api.pdf

WX12Kまでしか書かれてない…まぁ、ほぼ上位互換的に性能が上がってるはずなのでWX12Kで動けば動くでしょう(

 

機種情報の方のWX12Kの情報も参照しておきます。

www.ymobile.jp

 https://www.ymobile.jp/service/contents_service/common/pdf/function.pdf

f:id:kikyou_kiki:20180504165010p:plain

ほとんど同じですがサウンド関係についてはこちらに詳しく書かれているのでチェックしておきます。

スペシャルサンクス

f:id:kikyou_kiki:20180518003048p:plain
ばななん。さんに特殊タイトル画面イラストを描いていただきました。
ゲームに使用するイラストを他人に依頼することは初めての経験でしたので、大変勉強になりました。

依頼の仕方などで相手と文字でやりとりするにあたって、相手にどのような文章にして自分の要件を伝えることができるか、などを考えるきっかけになりました。

saraemi.com

キャラクターの設定資料などを用意していなかった中、(あの後ろ姿三枚のタイトル差分くらい)、今まで後ろ姿を向いていたキャラクターがこちらを向いて笑顔を見せるという場面を表現していただきました。

このブログを作った理由の一つに、この素晴らしいイラストがプレイヤーの限られるアプリ限定になってしまうのが勿体なかったというがあります。

ばななん。様へ制作に快く協力していただいたことを、この場にて感謝申し上げます。

使用した素材など

音楽:童謡「てるてる坊主」(MIDI)

音楽研究所 様

効果音:ゲーム用効果音「reflect」(wav)

TAM Music Factory 様

ゲームのBGM、SEに使用しました。

童謡てるてる坊主の童謡風、ハープアレンジをステージBGMとゲームオーバーBGMに使用。

かすり時のSEにはreflectを使用しています。

*1:アプリゲット等。今でいうAppStoreやGooglePlayのようなところ

Flaskで作ったマルチプレー募集掲示板『まる募』

私が製作したWebアプリケーションがついに完成し、公開までこぎつけられたので紹介します!

その名も、Twitter連動型マルチプレー募集掲示板、『まる募』。

f:id:kikyou_kiki:20210502220931p:plain

まる募 - Twitter連動マルチプレー募集掲示板 -

(テスト運用中。デバッガー募集中)

 

Twitterでゲームのマルチプレー仲間を募集している人を見かけたのが制作のきっかけ。

いろんな人とマルチプレーを楽しみたいけど、いろんな事情(フォロワーが少ないとか…)で難しいって人もいると思います。

まず最初に考えたのは、BotTwitter上に拡散することでいろんな人の目に留まるようにできないかということ。

そしてその募集情報を一覧で見られたら便利じゃね!?ってところから始まり、Twitter連動型募集掲示板という形に。

自分から誘いにくいって人でも、ここに書くことでマルチプレーを気軽に募集出来たり、飛び入り参加出来たりしたらいいなぁって思って。

開発はPython+Flask+MySQL

筆者、Webサービスを公開までするのはこれが初めて。Webはほぼ分野外だったので色々と大変でした……。(いちおう仕事でASP.NETを触ったことはある。)

 

Botも運用開始→(まる募Bot (@multiplayboard) | Twitter)

 

(2023/2/9追記)

昨今のTwitterAPI有料化のこともあり、サービスを終了しました!

拙いサービスでしたがご利用いただいたみなさまありがとうございました。

 

(2023/6/15追記)

ソースダウンロードはこちら

marubodiary.hateblo.jp

 

 

【Python/BeautifulSoup】スクレイピングで各話リストをつくるソースコード【ニコニコ大百科】

製作のきっかけ

推しアニメの各話リスト作りたいなーって話になり、せっかくキリの良い所を迎えたことだし作るか!と。
…って既に52話もあるんですが。1クール12話のアニメならともかくこれをコピペでも手動でやるのはしんどい、自動化できる部分自動化しよう。
などと呟きながら、過去のコードを参考にして適当に書き始めたのだった。
※このソースコードは今後のサイトリニューアルなどで使えなくなる可能性があります。

ソースコード

# coding: utf-8
from bs4 import BeautifulSoup
import re

def main():
    # htmlを指定
    nicohtml = ("ニコニコチャンネルのページを指定")
    dhtml = ["dアニメのページを指定",
            "複数ページにわたる場合は配列にして指定"]
    # 変数宣言
    nurlList = []   #動画URL(ニコニコ)
    nimgList = []   #サムネイル画像URL(ニコニコ)
    noList = []     #話数カウント
    titleList = []  #タイトル
    nIdList = []    #動画ID(ニコニコ)
    durlList = []   #動画URL(dアニメ)
    dimgList = []   #サムネイル画像URL(dアニメ)
    dIdList = []    #動画ID(dアニメ)
    # ニコニコデータ
    nurlList, nimgList, noList, titleList, nIdList = Niconico(nicohtml, nurlList, nimgList, noList, titleList, nIdList)
    # dアニデータ
    if(len(dhtml)>0):
        durlList, dimgList, dIdList = dAni(dhtml, durlList, dimgList, dIdList)
    # Tableタグ作成
    if(len(dhtml)>0):
        tableTag = TableMake(noList, titleList, nIdList, dIdList, nurlList, durlList, nimgList, dimgList)
    else:
        tableTag = TableMakeN(noList, titleList, nIdList, nurlList, nimgList)
        
    # txt出力
    outputPath = "保存先ファイルを指定する"
    MakeTxt(outputPath, tableTag)

def Niconico(html, nurlList, nimgList, noList, titleList, nIdList):
    soup = BeautifulSoup(open(html, encoding="utf-8"), 'lxml', from_encoding="utf-8")
    
    # <a class ="g-video-link">~</a>を取得
    videoLinks = soup.find_all("a","thumb_anchor g-video-link")

    count = 0
    for videoLink in videoLinks:
        data = videoLink
        # タイトル
        title = data.get("title") 
        if (title == None):
            continue
        titletxt = re.findall("「(.*)」", str(title).replace('\u3000', ' '))
        # URL
        nurl = data.get("href")
        # 画像URL
        nimg = data.find("img")["data-original"]
        # 動画ID(ニコニコ)
        nId = nurl.split("/")
        # 話数名
        count += 1

        nurlList.append(nurl)
        nimgList.append(nimg)
        noList.append("第" + str(count) + "話")
        titleList.append(titletxt[0])
        nIdList.append(nId[-1])

    return nurlList, nimgList, noList, titleList, nIdList

def dAni(html, durlList, dimgList, dIdList):
    for page in html:
        soup = BeautifulSoup(open(page, encoding="utf-8"), 'lxml', from_encoding="utf-8")
    
        # <div class ="item_left">を取得
        items = soup.find_all("div", "item_left")
        for item in items:
            data = item
            # URL
            durl = data.find("a").get("href")
            # 画像URL
            dimg = data.find("img")["data-original"]
            # 動画ID
            dId = durl.split("/")

            durlList.append(durl)
            dimgList.append(dimg)
            dIdList.append(dId[-1])

    return durlList, dimgList, dIdList

def TableMake(noList, titleList, nIdList, dIdList, nurlList, durlList, nimgList, dimgList):
    #htmlタグ生成
    Table = []
    Table.append("<table><tbody><tr>")
    nowidth = len(noList) // 10 * 8 + 16 # 話数で長さ調整
    Table.append("<th width =\"" + str(nowidth) + "\" style=\"text-align: center; vertical-align: middle;\">話数</th>")
    Table.append("<th style=\"text-align: center; vertical-align: middle;\">サブタイトル</th>")
    Table.append("<th style=\"text-align: center; vertical-align: middle;\">動画</th>")
    Table.append("<th style=\"text-align: center; vertical-align: middle;\">dアニメ</th>")
    Table.append("</tr>")
    for no, title, nid, did, nurl, durl, nimg, dimg in zip(noList, titleList, nIdList, dIdList, nurlList, durlList, nimgList, dimgList):
        Table.append("<tr>")
        Table.append("<td style=\"vertical-align: middle; text-align: center; \">" + no + "</td>")
        Table.append("<td style=\"vertical-align: middle;\">" + title + "</td>")
        Table.append("<td style=\"padding: 2px; text-align: center; vertical-align: middle;\">" + "<a style=\"background-image: none;\" href=\"" + nurl + "\" target=\"_blank\" title=\"" + title + "\" rel=\"nofollow\"><img style=\"margin: 0px 0px -5px 0px;\" src=\"" + nimg + "\" alt=\"動画\" width=\"65\" height=\"50\" /></a></td>")
        Table.append("<td style=\"padding: 2px; text-align: center; vertical-align: middle;\">" + "<a style=\"background-image: none;\" href=\"" + durl + "\" target=\"_blank\" title=\"" + title + "\" rel=\"nofollow\"><img style=\"margin: 0px 0px -5px 0px;\" src=\"" + dimg + "\" alt=\"dアニメ\" width=\"65\" height=\"50\" /></a></td>")
        Table.append("</tr>\n")
    Table.append("</tbody></table>")

    return Table

def TableMakeN(noList, titleList, nIdList, nurlList, nimgList):
    #htmlタグ生成
    Table = []
    Table.append("<table><tbody><tr>")
    nowidth = len(noList) // 10 * 8 + 16 # 話数で長さ調整
    Table.append("<th width =\"" + str(nowidth) + "\" style=\"text-align: center; vertical-align: middle;\">話数</th>")
    Table.append("<th style=\"text-align: center; vertical-align: middle;\">サブタイトル</th>")
    Table.append("<th style=\"text-align: center; vertical-align: middle;\">動画</th>")
    Table.append("</tr>")
    for no, title, nid, nurl, nimg in zip(noList, titleList, nIdList, nurlList, nimgList):
        Table.append("<tr>")
        Table.append("<td style=\"vertical-align: middle; text-align: center; \">" + no + "</td>")
        Table.append("<td style=\"vertical-align: middle;\">" + title + "</td>")
        Table.append("<td style=\"padding: 2px; text-align: center; vertical-align: middle;\">" + "<a style=\"background-image: none;\" href=\"" + nurl + "\" target=\"_blank\" title=\"" + title + "\" rel=\"nofollow\"><img style=\"margin: 0px 0px -5px 0px;\" src=\"" + nimg + "\" alt=\"動画\" width=\"65\" height=\"50\" /></a></td>")
        Table.append("</tr>\n")
    Table.append("</tbody></table>")

    return Table

def MakeTxt(outputFile, Tabledata):
    with open(outputFile, mode='w') as f:
        f.writelines(Tabledata)

if __name__ == "__main__":
    main()

コードの解説

def Niconico()

ニコニコチャンネルのページから各話情報を取得するためのメソッド。

    # <a class ="g-video-link">~</a>を取得
    videoLinks = soup.find_all("a","thumb_anchor g-video-link")

今回欲しいデータは、タイトル、サムネイルのURL、動画URL。
f:id:kikyou_kiki:20210124235727p:plain
これらはサムネイルに設定されている部分(上のthumb_anchor g-video-link)を取得すれば事足りるので、このタグを取得できる条件を設定します。

        # タイトル
        title = data.get("title") 
        if (title == None):
            continue

これは上記の検索条件だと上の方にある無料配信になっている動画の所が出力に余計に入ってきてしまうため、それを弾くために入れている条件。

titletxt = re.findall("「(.*)」", str(title).replace('\u3000', ' '))

「と」の間のタイトルを取得する処理。正規表現を使っています。今回の例の場合、
妖怪学園Y Nとの遭遇(妖怪ウォッチJam) 第1話「衝撃!! 初恋の人は○○だった」の「」内の部分、『衝撃!! 初恋の人は○○だった』だけを取得できます。
あと全角スペースが\u3000に置き換わっているのでついでに置き換えます。

        # URL
        nurl = data.get("href")
        # 動画ID(ニコニコ)
        nId = nurl.split("/")

        nIdList.append(nId[-1])

URLを/区切りでカットした配列を作り、末尾のみを取得します。
これで動画IDのみを取得できます。(が今回は取得しただけで使っていない)
サムネイルじゃなくて埋め込みコードを使いたいときは使えるかも。

        noList.append("第" + str(count) + "話")

ループで話数のテキストを作っています。
もし取得したいアニメのエピソードが、#〇とか第〇羽とかだったらここを書き換えます。

def dAni()

上記のdアニメページ版。
大体同じですが、こちらは複数ページを取得する可能性があるので、ループ内でページごとにbeautifulSoupで取得。

        # <div class ="item_left">を取得
        items = soup.find_all("div", "item_left")

こちらは画像URL、URLのデータを取得できる部分を指定します。

def TableMake()&def TableMakeN()

ここでhtmlタグを作成します。
dアニメのURLが指定されているがされていないかで分岐します。

<th style=\"text-align: center; vertical-align: middle;\">

でヘッダー行の書式を指定。

Table.append("<td style=\"padding: 2px; text-align: center; vertical-align: middle;\">" + "<a style=\"background-image: none;\" href=\"" + nurl + "\" target=\"_blank\" title=\"" + title + "\" rel=\"nofollow\"><img style=\"margin: 0px 0px -5px 0px;\" src=\"" + nimg + "\" alt=\"動画\" width=\"65\" height=\"50\" /></a></td>")
Table.append("<td style=\"padding: 2px; text-align: center; vertical-align: middle;\">" + "<a style=\"background-image: none;\" href=\"" + durl + "\" target=\"_blank\" title=\"" + title + "\" rel=\"nofollow\"><img style=\"margin: 0px 0px -5px 0px;\" src=\"" + dimg + "\" alt=\"dアニメ\" width=\"65\" height=\"50\" /></a></td>")

特にここのstyle="~"やwidth="~",height="~"は出力結果を見ながらパラメータ調整します。
ちなみにここのa属性のstyle="~”を指定しないと、
f:id:kikyou_kiki:20210125000347p:plain
左のように右下にアイコンがある感じになります。

使い方・実行サンプル

諸注意

このソースコードを実行したことによる問題について、筆者は責任を負いかねますので使用する場合は自己責任で。
あと妖怪学園Yのページ以外では動作確認していません。(オイ

各話リストが欲しいアニメのニコニコチャンネルのページを確認します

https://ch.nicovideo.jp/youkai-watch2020
こんな感じのページですね。
dアニメストア支店のデータも必要な場合は検索して持ってきます、
https://ch.nicovideo.jp/search/妖怪学園Y?&mode=s&sort=f&order=a&type=video&channel_id=ch2632720&page=1
検索条件は『投稿が古い順』に設定します。
筆者はリアルタイム性が必要ないスクリプトの場合、サーバーに負担をかけないようページを『名前を付けて保存』してからスクレイピングを実行する派なので、これらを保存します。
marubodiary.hateblo.jp
保存したHTMLを使う場合、こちらも参考資料として。

ファイルを指定します

保存したhtmlファイルの保存先を、

    # htmlを指定
    nicohtml = ("ニコニコチャンネルのページを指定")
    dhtml = ["dアニメのページを指定",
            "複数ページにわたる場合は配列にして指定"]

dアニメストアの方のhtmlがない場合は空にする。
出力結果のtxtファイルを保存する場所を

    # txt出力
    outputPath = "保存先ファイルを指定する"

にファイル名含むフルパスで指定します。拡張子はtxt推奨。

実行します

実行すると指定したところに話数、サブタイトル、動画のサムネイル(リンク付き)のTableタグが出力されたtxtファイルが出力されます。
これを大百科のHTMLエディタに貼り付けてみると…
dアニメ指定あり
f:id:kikyou_kiki:20210124235526p:plain
dアニメ指定なし
f:id:kikyou_kiki:20210124235532p:plain

感想

今思えば変数クラス化しときゃ良かったわ。後付けしつつ適当に作ったせいでその辺の設計ガバってた。
各話リスト作る過程でmarginとか画像サイズとかの部分をいろいろ試行錯誤してたのですが、
実行したら52話分一発でレイアウトとかを変えられるのはすごく楽だった。自動化して良かった。一個一個やってたら発狂してるとこだったわ。
せっかく作ったものの、筆者は一般会員で全く活用出来そうにないので、好きだけど話数が多すぎて編集するのがしんどい…ってアニメがあるニコ百編集者様方、ぜひこのソースを活用して頂ければ。
GUI化して設定項目とか分かりやすく設定できるようにしたら需要あるかなぁ?そもそもの目的がニッチすぎるか。

あと妖怪学園Yを見てください。このアニメが無かったらこのソースコードは生まれなかった。
せめてOPだけでもいいので(ry

学園が炎上するところから始まる公式紹介ムービーも紹介しておきますね。

ローカル保存HTMLのスクレイピング覚書

ネット上にあるHTMLのスクレイピングに関してはかなり情報があるが、

ローカル上にあるHTMLファイルのスクレイピングについてはあまり情報がなかったので覚書。

ファイルの読み込み

Pythonの場合

soup = BeautifulSoup(open(ローカルファイルの保存場所))

あとは通常通り。

C#の場合

通常のテキストファイルと同じくFileStream()を使う。

String型にできればあとは通常通り。

HTMLファイルの形式がMHTMLである場合

Webページを単一の完全なファイルで保存できるMHTML形式のファイルは、文字がQuoted-printableエンコードされているため、扱うにはデコードする必要がある。

Edge以外何かしらのブラウザが使える環境なら、そのmhtmlを開きなおす→html形式で保存しなおしても直るので、単一の完全なWebページでなくて大丈夫な時はそれでもOK。正直それが手っ取り早そう

どうしてもmhtmlで処理しなければならない場合。

上記の方法で読み込み、ヘッダーを削除してからデコード処理した方がよさそう、<body>~</body>を抜き出してからデコードを行うのがよい。

Quoted-printableからのデコード方法

Pythonの場合

quopri --- MIME quoted-printable 形式データのエンコードおよびデコード — Python 3.9.0 ドキュメント

C#の場合

[C#] QuotedPrintable エンコード・デコードの実装方法 │ Web備忘録

いずれもbyteで帰ってくるためUTF-8等任意のエンコードに変換する。

技術ブログ始めました

また新しくブログを作りました。

過去に公開していたブログをひっそり一つ潰したので、その内容の一部の移転先確保と、今趣味でやってるブログに無理矢理突っ込まれていた技術関連を引き抜いて、今作ってるアプリに関する話は今後こちらに書いていきたいと思います。

内容としては最近は趣味でWebアプリ開発とかやってたりするのでその開発で苦労したこと、解決策とか、こんなアプリ欲しいなぁって漠然と考えているアイデアメモとか。

あと他には読んだ技術本でこれはわかりやすかった!というようなアウトプットをしていきたいですね。