Jewel-mmo開発日記

RubyでMMORPGを作る過程を記録する日記。 Yokohama.rb 発起人。
2010-03-11

[ruby][MyGame]MyGameで書いた3Dぽいテニスゲームのプロトタイプ

よくあるタイプのテニスゲームで座標を3Dで管理してる。 カメラを設けてシンプルに3Dを管理するという原理はこれと同じ。

Rubyでやりたい3D制御をイメージして書いたコードだったと思う。 たぶん3年くらい前に書いたコード。 当時は動かしながら書いていたけど、今はどうかな。動くかどうかは試してない。動かない可能性が高いと思う。

DxRubyが3Dに対応しつつあるようなので、もしかしたら何かの参考になるかもしれないと思って古いコードを引っ張りだしてみた。

# tennis.rb
require 'mygame/boot'
require 'tennis_helper'

COURT_H = 1189
COURT_W = 1097

class Player < Character
  image_resource 'images/player.bmp'
end

class Net < Character
  image_resource 'images/net.bmp'
end

class BallShadow < Character
  image_resource 'images/ball_shadow.bmp'
  attr_accessor :parent
  def update
    position.x = parent.position.x
    position.z = parent.position.z
  end
end

class Ball < Character
  image_resource 'images/ball.bmp'
  def initialize
    super
    start
  end

  def start
    @position = Vector3D[400, -200, 1300]
    @v = Vector3D[-4 - rand(6), -16 + rand(4), -10 - rand(20)]
    #@position *= [-1, 1, -1]; @v *= [-1, 1, -1]
  end

  def update
    @v.y += 0.5
    @position += @v
    if position.y > -10
      position.y = -10
      e = 0.7
      @v *= [e, -e, e]
    end
    if position.z.abs > 1500 or @v.z.abs < 0.1
      start
    end
  end
end

camera = Camera.new
players = Array.new(4) {|i|
  a = Player.new
  a.position.x = i % 2 == 0 ? -COURT_W / 2 : COURT_W / 2
  a.position.z = i / 2 == 0 ? -COURT_H : COURT_H
  a
}
players = []
net = Net.new
ball = Ball.new
ball_shadow = BallShadow.new
ball_shadow.parent = ball
bg = Image.new('images/bg.bmp')

MyGame.background_color = nil
g = Ground.new(camera)
main_loop(60) do
  [ball, ball_shadow].each {|e| e.update }
  bg.render
  camera.render(players, net, ball_shadow, ball)
  g.render
end

2Dの絵を使うのでcameraの向きが固定になっている(背景も2Dなので視点はほぼ固定)。なのでcameraに注視点の指定やアングルの指定はない。 cameraに注視点またはアングルが指定できれば、上記スクリプトで一応ぐりぐり回るはずだ(もちろんその場合は2D画像でなく3Dモデルを読み込む必要がある)。

当時何をやりたかったかと言うと、こんな風に書けるライブラリがあれば3Dのプログラミングも全然難しくないでしょって言いたかったんだと思う。

上記ソースが使うヘルパの実装もはっておく。

# tennis_helper.rb
class Vector3D < Array
  %w(x y z).each_with_index do |e, i|
    eval "def #{e} ; self[#{i}] ; end"
    eval "def #{e}=a ; self[#{i}] = a ; end"
  end
  %w(+ - * /).each do |e|
    eval "def #{e}d
            Vector3D[self[0] #{e} d[0], self[1] #{e} d[1], self[2] #{e} d[2]]
          end"
  end

  include Math
  def rotate_zyx(angle)
    ax, ay, az = angle
    px, py, pz = x, y, z
    px, py = px * cos(az) - py * sin(az), px * sin(az) + py * cos(az)
    pz, px = pz * cos(ay) - px * sin(ay), pz * sin(ay) + px * cos(ay)
    py, pz = py * cos(ax) - pz * sin(ax), py * sin(ax) + pz * cos(ax)
    Vector3D[px, py, pz]
  end
end

class Camera
  attr_accessor :position, :angle, :scale, :screen_z
  def initialize
    @position = Vector3D[0, -200, -2000]
    @angle = Vector3D[Math::PI / 20, 0, 0]
    @screen_z = 800.0
    @scale = 1.0
  end

  def to_screen(vector3d)
    temp = (vector3d - position).rotate_zyx(angle)
    perse = screen_z / temp.z
    [temp.x * perse + screen.w / 2, temp.y * perse + screen.h / 2, perse * @scale]
  end

  def render(*objects)
    objects.to_a.flatten.each do |e|
      e.view.x, e.view.y, e.view.scale = to_screen(e.position)
      e.render
    end
  end
end

class Character
  attr_accessor :position
  attr_reader :view
  def self.image_resource(fname)
    class_eval "def image_file_name; '#{fname}'; end"
  end

  def initialize
    @view = TransparentImage.new(image_file_name)
    @position = Vector3D[0, 0, 0]
  end

  def update
    @view.update
  end

  def render
    @view.render
  end
end

class Ground
  def initialize(camera)
    @camera = camera
    @tops = [
      Vector3D[-1000, 0, -1000],
      Vector3D[ 1000, 0, -1000],
      Vector3D[ 1000, 0,  1000],
      Vector3D[-1000, 0,  1000],
    ]
  end

  def render
    tops_2d = @tops.map {|top| @camera.to_screen(top) }
    # @view.render ...
  end
end