蟻地獄

Twitterに書ききれない長めの文とか書くよ

WebGLで作った3DネトゲにjQueryで吹き出しチャットを付けてみた

ネトゲっていうかまだスタンドアロンなんだけど、WebGLで作った3D空間の任意の位置にDOM要素の吹き出し重ねたりできるかな?ってやってみたら結構簡単にできました。

動かし方

前回の記事と同様に、WebGLが動くブラウザを用意して下さい。
最新のFirefox開発版で動作確認済みです。Chromiumでも動くけど、最新版だとなんかオクルージョンが変だったり動作が怪しいのでFirefoxのほうがいいかも。
で、ブラウザの準備ができたら下記のURLにアクセス!チャットを入力するとねこさんが喋ります。
http://meengr.sakura.ne.jp/demo2/

解説

吹き出しはWebGLのテクスチャではなく、DOM要素を動的に追加してCanvasに絶対位置指定で重ねています。毎フレームねこさんの位置からウィンドウ座標を計算し、適切な位置に移動させています。詳しくはソースを見てね。
でもこれってパフォーマンス的にどうなんだろう?けっこう重い気がする・・・

WebGLでヌルヌル動く3Dネトゲを作ってみた

タイトルは釣りだけど、WebGLで3DネトゲっぽいUIを作ってみたよ!

動かし方

まだWebGLに正式対応したブラウザが無いから、開発版のブラウザをインストールする必要があります。めんどくさい!
詳しくはここらへんを見てね。
Getting a WebGL Implementation - WebGL Public Wiki

ここでは一番簡単に使えるChromiumのインストールのしかたを説明します。

  1. ダウンロードページ(http://build.chromium.org/buildbot/continuous/win/LATEST/)から最新版をダウンロード
  2. 以下のようにコマンドラインオプションを付けて起動

chrome.exe --no-sandbox --enable-webgl

コマンドラインオプションを付けないとWebGLが有効にならないので注意してください。
あと、OpenGL2.0に対応したグラフィックボードが必要だから、ノートPCとかじゃできないかもよ。

で、ブラウザの準備ができたら下記のURLにアクセス!
http://meengr.sakura.ne.jp/demo/

作り方

ソースを見てね☆
使ったライブラリは以下の通り。

GLGE WebGL Library/Framework

WebGLのラッパー。ライティングとかピッキングとかこれ一発でできて便利。

jQuery UI

チャットウィンドウとかをDraggableにするのに使用。
こういう既存のライブラリが使えるから、WebGLでゲーム作るとUIの実装がすごく簡単です。

http://www.kelvinluck.com/assets/jquery/jScrollPane/jScrollPane.html

デフォルトのスクロールバーだと見栄えが悪かったので変えるのに使用。

タイヤ買取ナンバーワン、ホイール買取

イコン画像を使わせていただきました。ありがとう!

Cometeoのサーバ構成を晒してみる

スケールアウトからスケールアップへの回帰:Kenn's Clairvoyance - CNET Japanに対抗して、
Lingrのクローンというかパクりの超高速無料レンタルチャットCometeoのサーバ構成を晒しちゃうよ!


サーバ: さくらインターネット 専用サーバ エントリープラン
CPU: Atom 1.6GHz
メモリ: 1GB
HDD: 160GB
回線: 10M共有
料金: 月額7,800円


これにMySQLフルスクラッチhttpd(C++で書いた!)が乗ってる。
DBのバックアップは毎晩AmazonS3に転送して直近7日分を保管。S3の月額は1ドル未満。

で、サービス状況はというと、


月間PV: 8万くらい
同時接続数: ピーク時で350〜400くらい
総チャットルーム数: 5500くらい
総発言数: 340万くらい
CPU: スカスカ
HDD: スカスカ
メモリ: 余裕
回線: 1ルームに200人とか来た日は辛いけど普段はスカスカ


で、ビジネス的にはどうかというと、


アフィリエイト収入: 月3,000円くらい

全然赤字だよ☆

GoでCometチャットサーバを書いてみた

最近流行りのGoでCometなチャットサーバを書いてみたよ!
いちおう動いてるっぽいけど正しいかは知らないよ!

http://www.cometeo.com:8080/

package main

import (
	"flag";
	"http";
	"log";
	"container/list";
	"strconv";
	"bytes";
	"json";
	"time";
)

type post struct {
	id uint64;
	name string;
	comment string;
	at *time.Time;
}

type chatData struct {
	lastId uint64;
	posts list.List;
	waits list.List;
}

type request interface {
	execute(data *chatData);
}

type addRequest struct {
	name string;
	comment string;
}

type getRequest struct {
	id uint64;
	resCh chan []post;
}

func (req addRequest) execute(data *chatData) {
	data.lastId++;
	p := post{data.lastId, req.name, req.comment, time.LocalTime()};
	data.posts.PushBack(p);
	if data.posts.Len() > 100 {
		data.posts.Remove(data.posts.Front());
	}

	for ch := range data.waits.Iter() {
		res := make([]post, 1);
		res[0] = p;
		ch.(chan []post) <- res;
	}
	data.waits.Init();
}

func (req getRequest) execute(data *chatData) {
	if req.id == data.lastId {
		data.waits.PushBack(req.resCh);
	}else{
		i := 0;
		e := data.posts.Front();
		for ; e != nil; i,e = i+1,e.Next() {
			if e.Value.(post).id > req.id {
				break;
			}
		}
		res := make([]post, data.posts.Len()-i);
		for i = 0; e != nil; i,e = i+1,e.Next() {
			res[i] = e.Value.(post);
		}
		req.resCh <- res;
	}
}

var requestCh = make(chan request)
var addr = flag.String("addr", ":8080", "http service address")

func backend() {
	var data chatData;
	for {
		req := <- requestCh;
		req.execute(&data);
	}
}

func add(c *http.Conn, req *http.Request) {
	requestCh <- addRequest{req.FormValue("name"), req.FormValue("c")};
	c.WriteHeader(http.StatusOK);
}

func get(c *http.Conn, req *http.Request){
	id,err := strconv.Atoui64(req.FormValue("id"));
	if err != nil {
		c.WriteHeader(http.StatusBadRequest);
	}else{
		q := getRequest{id, make(chan []post)};
		requestCh <- q;
		res := <- q.resCh;

		c.SetHeader("Content-Type", "application/json; charset=utf-8");
		var buf bytes.Buffer;
		buf.WriteString("{\"post\":[");
		last := len(res)-1;
		for i,val := range res {
			buf.WriteString("{\"id\":" + strconv.Uitoa64(val.id) 
				+ ",\"name\":" + json.Quote(val.name) 
				+ ",\"c\":" + json.Quote(val.comment) 
				+ ",\"at\":"+ json.Quote(val.at.String()) + "}");
			if(i != last){
				buf.WriteString(",");
			}
		}
		buf.WriteString("]}");
		c.Write(buf.Bytes());
	}
}

func main() {
	flag.Parse();

	http.Handle("/", http.FileServer("www", ""));
	http.Handle("/add", http.HandlerFunc(add));
	http.Handle("/get", http.HandlerFunc(get));

	go backend();

	err := http.ListenAndServe(*addr, nil);
	if err != nil {
		log.Exit("ListenAndServe:", err);
	}
}