前提
画像を回転、拡大、反転表示する順番を決めて表示できる人が対象(この記事ではpicture)。あとそれを一つのクラス(この記事ではhyojiman)に集結させて表示させられるようになっている。
最終形はこんな感じになります。
同じような機能がUnityに既にあるのでそれを使いましょう。
どうアニメーションするの?
古のアニメーションと言えば、マリオを思い浮かべていただけるとわかりやすいのですが一枚絵の状態を切り替えることによってアニメーション作っていました。まあそれは非常にめんどくさいのでMMDのようにボーンとか体の部位を決めて回転とかして動かすのがトレンドです。
(MMDなんて触ったことないから全部予想ですが)
関節を作ってそれぞれに名前を付けます。それをtreeにして一番上の根っこをcharacterとします。一応ゲームに使うようなのでヒットボックスをcharacterに記しておきましょう。
アニメーションはmotionというクラスを作りそれをcharacterに持たせて動かすことでできるでしょう。motionは複数のmove(関節を動かすとか大きさを変えるとか)を持ち順番に動作させます。
後はキャラクターごと反転させる処理をしたり、ペーパマリオの回転みたいな動きをできるようにしたり、とても大変です。しかししかあり動作してくれた時の感動はひとしおです。
ちなみにかなり間違ってる部分があると思うので改善した方がいいと思います。
characterの概要!
だいたいコードはこんな感じになります。
[Serializable]//ファイルとして保存するためにつけてるよ
public class setu
{
public string nm;//名前
public picture p;//画像一枚を書く奴
public List<setu> sts = new List<setu>();//ツリーを構成するため
public float dx, dy;//親からの距離
public setu(string name, float ddx, float ddy, picture pic, List<setu> kansetu)
{
nm = name;
dx = ddx;
dy = ddy;
p = pic;
sts = kansetu;
}
public setu(setu s)//コピーのためのコンストラクタ
{
nm = s.nm;
p = new picture(s.p);
foreach (var a in s.sts)
{
sts.Add(new setu(a));
}
dx = s.dx;
dy = s.dy;
}
public setu() { }//保存のためのコンストラクタ
public setu GetSetu(string name)//自分以下から同名の節を探す。自己参照っぽい感じ
{
if (nm == name) return this;
setu res = null;
for (int i = 0; i < sts.Count; i++)
{
var tmp = sts[i].GetSetu(name);
if (tmp != null) res = tmp;
}
return res;
}
public setu Getrootsetu(string name)//これは親を探し出す奴
{
setu res = null;
for (int i = 0; i < sts.Count; i++)
{
if (sts[i].nm == name) return this;
var tmp = sts[i].Getrootsetu(name);
if (tmp != null) res = tmp;
}
return res;
}
public void Removesetu(string name, hyojiman hyojiman)
{
if (name == nm)
{
for (int t = sts.Count() - 1; t >= 0; t--)
{
Removesetu(sts[t].nm, hyojiman);
}
}
for (int i = sts.Count() - 1; i >= 0; i--)
{
sts[i].Removesetu(name, hyojiman);
if (sts[i].nm == name)
{
hyojiman.removepicture(sts[i].p);
sts.RemoveAt(i);
}
}
}
public List<setu> getallsetu()
{
var l = new List<setu>();
foreach (var kk in sts)
{
l.AddRange(kk.getallsetu());
}
l.Add(this);
return l;
}
public void frame()//節の位置を揃える。
{
foreach (var a in sts)
{
if (p.mir)
a.p.settxy(p.x + (p.w - a.dx) * (float)Math.Cos(p.RAD) - (a.dy) * (float)Math.Sin(p.RAD)
, p.y + (p.w - a.dx) * (float)Math.Sin(p.RAD) + (a.dy) * (float)Math.Cos(p.RAD));
else
a.p.settxy(p.x + (a.dx) * (float)Math.Cos(p.RAD) - (a.dy) * (float)Math.Sin(p.RAD)
, p.y + (a.dx) * (float)Math.Sin(p.RAD) + (a.dy) * (float)Math.Cos(p.RAD));
a.frame();
}
}
public void scalechange(float sc)
{
foreach (var a in getallsetu())
{
a.p.w *= sc;
a.p.h *= sc;
a.p.tx *= sc;
a.p.ty *= sc;
a.dx *= sc;
a.dy *= sc;
}
}
}
[Serializable]//ファイルとして保存するためにつけてるよ
public class character
{
//ヒットボックスのためのパラメータ
public float x;
public float y;
public float w;
public float h;
protected double rad;
public float tx;
public float ty;
//節の根元
public setu core;
//モーションのリスト
protected List<motion> motions = new List<motion>();
protected character kijyun = null;//反転処理、初期化とか行う際に必要
public bool mirror { get { return _mirror; } set { _mirror = value; } }
protected bool _mirror = false;
public double RAD { get { rad = Math.Atan2(Math.Sin(rad), Math.Cos(rad)); return rad; } set { rad = Math.Atan2(Math.Sin(rad), Math.Cos(rad)); float x = gettx(), y = getty(); rad = value; settxy(x, y); } }
public character(float xx, float yy, float ww, float hh, float ttx, float tty, double sita, setu cor)
{
x = xx;
y = yy;
w = ww;
h = hh;
tx = ttx;
ty = tty;
rad = sita;
core = cor;
setkijyuns();
}
public void setkijyuns()//基準をセットする
{
kijyun = new character(this, false);
kijyun.kijyun = null;
}
public character getkijyun()
{
if (kijyun != null) return kijyun;
return this;
}
public void resettokijyun(hyojiman hyo)
{
character c = getkijyun();
w = c.w;
h = c.h;
tx = c.tx;
ty = c.ty;
rad = c.rad;
_mirror = c._mirror;
this.sinu(hyo);
core = new setu(kijyun.core);
this.resethyoji(hyo);
premir = false;
}
public void resettokijyun()//基準と同じ姿に戻す
{
character c = getkijyun();
w = c.w;
h = c.h;
tx = c.tx;
ty = c.ty;
rad = c.rad;
_mirror = c._mirror;
premir = false;
foreach (var a in kijyun.core.getallsetu())
{
var b = this.core.GetSetu(a.nm);
if (b != null)
{
b.dx = a.dx;
b.dy = a.dy;
b.p.x = a.p.x;
b.p.y = a.p.y;
b.p.z = a.p.z;
b.p.w = a.p.w;
b.p.h = a.p.h;
b.p.tx = a.p.tx;
b.p.ty = a.p.ty;
b.p.mir = a.p.mir;
b.p.OPA = a.p.OPA;
b.p.texname = a.p.texname;
b.p.textures = new Dictionary<string, string>(a.p.textures);
b.p.RAD = a.p.RAD;
}
}
}
public void refreshtokijyun(hyojiman hyo)//節も含め角度は同じで大きさや中心だけ基準と同じにする
{
character c = getkijyun();
character pre = new character(this, false);
w = c.w;
h = c.h;
tx = c.tx;
ty = c.ty;
rad = c.rad;
premir = false;
this.sinu(hyo);
core = new setu(kijyun.core);
this.resethyoji(hyo);
this.copykakudo(pre);
//こっちの場合hyojimanが与えられてるのでresethyojiで楽々
}
public void refreshtokijyun()
{
character c = getkijyun();
character pre = new character(this, false);
w = c.w;
h = c.h;
tx = c.tx;
ty = c.ty;
rad = c.rad;
premir = false;
//こっちの場合hyojimanが与えられてないので今あるインスタンスを改造する石かなく面倒
foreach (var a in kijyun.core.getallsetu())
{
var b = this.core.GetSetu(a.nm);
if (b != null)
{
b.dx = a.dx;
b.dy = a.dy;
b.p.x = a.p.x;
b.p.y = a.p.y;
b.p.z = a.p.z;
b.p.w = a.p.w;
b.p.h = a.p.h;
b.p.tx = a.p.tx;
b.p.ty = a.p.ty;
b.p.mir = a.p.mir;
b.p.OPA = a.p.OPA;
b.p.texname = a.p.texname;
b.p.textures = new Dictionary<string, string>(a.p.textures);
b.p.RAD = a.p.RAD;
}
}
this.copykakudo(pre);
}
public character(character c, bool setkijyun = true,bool motion=false)//コピーのためのコンストラクタ
{
x = c.x;
y = c.y;
w = c.w;
h = c.h;
tx = c.tx;
ty = c.ty;
rad = c.rad;
core = new setu(c.core);
_mirror = c._mirror;
premir = c.premir;
if (setkijyun)
{
if (c.kijyun == null)
{
setkijyuns();
}
else
{
kijyun = new character(c.kijyun, false);
}
}
if (motion) copymotion(c);
}
public void copymotion(character c)//モーションを他のキャラクターからコピーする
{
foreach (var a in c.motions)
{
motions.Add(new motion(a));
}
}
public void copykakudo(character c)//節も含め角度をコピーしてくる
{
foreach (var a in c.core.getallsetu())
{
foreach (var b in core.getallsetu())
{
if (a.nm == b.nm) b.p.RAD = a.p.RAD;
}
}
rad = c.rad;
}
public character() { }//保存のためのコンストラクタ
public void resethyoji(hyojiman hyojiman)//すべての節を表示する奴に登録する
{
var l = core.getallsetu();
foreach (var lll in l)
{
hyojiman.addpicture(lll.p);
}
}
virtual public void sinu(hyojiman hyojiman)//すべての節を表示する奴から消す
{
var l = core.getallsetu();
foreach (var lll in l)
{
hyojiman.removepicture(lll.p);
}
}
public void zchanged(hyojiman hyojiman)
{
var l = core.getallsetu();
foreach (var lll in l)
{
hyojiman.addpicture(lll.p);
}
}
protected bool premir = false;
virtual public void frame(float cl=1)
{
if (premir)
{
premir = false;
kijyuhanten();//反転してる場合こうやって無理やりモーションも反転させるようにしてるよ
}
for (int i = motions.Count() - 1; i >= 0; i--)//モーション共のフレーム
{
motions[i].frame(this,cl);
if (motions[i].owari)
{
motions.RemoveAt(i);
}
}
if (mirror)
{
premir = true;
kijyuhanten();
}
soroeru();
core.frame();
}
void kijyuhanten()//基準をもとに反転する
{
var lis = core.getallsetu();
foreach (var a in lis)
{
a.p.mir = !a.p.mir;
}
var kijyun = getkijyun();
{
foreach (var a in lis)
{
var b = kijyun.core.GetSetu(a.nm);
if (b != null)
{
var ki = a.p.RAD - b.p.RAD;
a.p.RAD = b.p.RAD - ki;
}
}
}
{
var ki = RAD - kijyun.RAD;
RAD = kijyun.RAD - ki;
}
}
public void soroeru()//節の位置を揃える
{
float txt = tx, tyt = ty;
if (mirror) txt = w - txt;
if (!core.p.mir)
{
core.p.settxy(x + (txt + core.dx) * (float)Math.Cos(rad) - (tyt + core.dy) * (float)Math.Sin(rad)
, y + (txt + core.dx) * (float)Math.Sin(rad) + (tyt + core.dy) * (float)Math.Cos(rad));
}
else
{
core.p.settxy(x + (w - txt - core.dx) * (float)Math.Cos(rad) - (tyt + core.dy) * (float)Math.Sin(rad)
, y + (w - txt - core.dx) * (float)Math.Sin(rad) + (tyt + core.dy) * (float)Math.Cos(rad));
}
//節は基本画像の右上を0として考えているが一番上だけ回転中心を0だと考えている。これは電子がマイナスの電荷をもっていることにしたぐらいの最大の間違いなので直してもいいかも
core.frame();
}
public void scalechange(float sc,bool setkijyun=true)//体の大きさを変えるついでにその時点で基準もセットできちゃう
{
w *= sc;
h *= sc;
tx *= sc;
ty *= sc;
core.scalechange(sc);
if(setkijyun)setkijyuns();
}
public float getcx(float ww, float hh)//ヒットボックスの任意の地点のx座標を返す
{
float rx = x + ww * (float)Math.Cos(rad) - hh * (float)Math.Sin(rad);
return rx;
}
public float getcy(float ww, float hh)
{
float ry = y + ww * (float)Math.Sin(rad) + hh * (float)Math.Cos(rad);
return ry;
}
public float
{
return x + tx * (float)Math.Cos(rad) - ty * (float)Math.Sin(rad);
}
public void setcxy(float xx, float yy, float cx, float cy)
{
x += xx - getcx(cx, cy);
y += yy - getcy(cx, cy);
soroeru();
}
public float getty()
{
return y + tx * (float)Math.Sin(rad) + ty * (float)Math.Cos(rad);
}
public void settxy(float xx, float yy)//中心点を指定地点に移動させる
{
x = xx - tx * (float)Math.Cos(rad) + ty * (float)Math.Sin(rad);
y = yy - tx * (float)Math.Sin(rad) - ty * (float)Math.Cos(rad);
soroeru();//移動したらそろえておく
}
//スライドするイメージの移動
public void wowidouxy(float dx, float dy)
{
settxy(gettx() + dx * (float)Math.Cos(rad) - dy * (float)Math.Sin(rad),
getty() + dx * (float)Math.Sin(rad) + dy * (float)Math.Cos(rad));
}
//表示順番であるzの幅を広げる
public void zbai(float bai)
{
foreach (var c in core.getallsetu())
{
c.p.z *= bai;
}
}
public void resetmotion()
{
motions.Clear();
}
public void addmotion(motion m, bool sento = true)
{
if (sento) motions.Insert(0, m);
else motions.Add(m);
m.start(this);
}
public void removemoves(Type t)
{
foreach (var a in motions) a.removemoves(t);
}
}
はい。私もこう乗せててよくわからなくなりました。ですがまだあります。
[Serializable]
public class motion
{
public List<moveman> moves = new List<moveman>();
protected int idx = 0, sidx = 0;
public float sp = 1;//モーションのスピード
public bool loop = false;
public bool owari { get { return moves.Count <= sidx; } }//終了の条件
public motion() { }
public motion(moveman mv) { addmoves(mv); }//一個だけmoveがあるモーションをちょっと作るときに便利
public motion(motion m)
{
idx = m.idx;
sidx = m.sidx;
loop = m.loop;
foreach (var a in m.moves)
{
var t = a.GetType();
moves.Add((moveman)Activator.CreateInstance(t, a));
}
}
public void start(character c)//モーション起動
{
idx = 0;
sidx = 0;
if (!owari) moves[idx].start(c);
}
public void frame(character c,float cl=1)
{
if (sp <= 0)
{
sidx = moves.Count();
return;
}
if (!owari)
{
for (int i = sidx; i <= idx && i < moves.Count; i++)//sidxとidxに挟まれてる奴が実行されるイメージ
{
if (!moves[i].owari)
{
moves[i].frame(c, sp*cl);
}
else if (sidx == i)//
{
sidx++;
}
if (!moves[i].STOP && i == idx)//moveがStopの性質を持ってるなら読み込みを止める。
{
idx++;
if (idx < moves.Count()) moves[idx].start(c);//startは読み込むってこと
}
}
}
if (owari && loop) start(c);//loopがtrueならもう一回繰り返す
}
public void addmoves(moveman m)//moveを加える
{
moves.Add(m);
}
public void addmovesikkini(motion m, int kai = 1)//ほかのモーションのmoveを加える。
{
for (int i = 0; i < kai; i++)
foreach (var a in m.moves)
{
var t = a.GetType();
moves.Add((moveman)Activator.CreateInstance(t, a));
}
}
public void removemoves(Type t)
{
for (int i = moves.Count() - 1; i >= 0; i--)
{
if (moves[i].GetType() == t || moves[i].GetType().IsSubclassOf(t)) moves.RemoveAt(i);
}
}
}
[Serializable]
public class moveman
{
public float time;
protected float timer;
public bool st;
public bool owari { get { return timer >= time; } }
public bool STOP { get { return st && !owari; } }
public float getnokotime(float sp, float from = -1)
{
if (from < 0) from = time;
var noko = from - timer;
if (noko < 0) noko = 0;
if (noko < sp) return noko;
return sp;
}
public moveman(float t, bool stop = false)
{
time = t;
timer = 0;
st = stop;
}
public moveman(moveman m)
{
time = m.time;
timer = m.timer;
st = m.st;
}
public moveman() { }
virtual public void start(character c)
{
timer = 0;
}
virtual public void frame(character c, float cl)
{
timer += cl;
}
}
そしてmovemanを継承して色々作ります。ここが地獄です。一つだけ例を載せます
[Serializable]
public class setumageman : moveman
{
public string nm;//曲げる節の名前
public double radto;//目指す角度
public double radsp;//回転速度
public bool sai;//最短距離で回転するか
protected setu tag;//曲げる節の本体
protected setu pretag;//曲げる節の親
protected List<setu> tags = new List<setu>();//曲げる節より下の節
public setumageman(float t, string name, double sitato, double sitasp, bool saitan = true, bool stop = false) : base(t, stop)
{
radto = Math.PI * sitato / 180;
radsp = Math.PI * sitasp / 180;
nm = name;
sai = saitan;
}
public setumageman(setumageman s) : base(s)//コピーするためのコンストラクタ
{
radto = s.radto;
radsp = s.radsp;
nm = s.nm;
sai = s.sai;
tag = s.tag;
pretag = s.pretag;
tags = new List<setu>(s.tags);
}
public setumageman() { }
public override void start(character c)
{
base.start(c);
tag = c.core.GetSetu(nm);
pretag = c.core.Getrootsetu(nm);
if (tag != null)
{
tags = tag.getallsetu();
}
else
{
tags = new List<setu>();
}//曲げるターゲットたちを見つける
}
public override void frame(character c, float cl)
{
var t = getnokotime(cl);
if (tag != null)
{
double rkaku;
if (pretag != null)//pretag==nullの時は親がcharacterの時
{
rkaku = pretag.p.RAD;
}
else
{
rkaku = c.RAD;
}
{//速度>=回転する角度(今の角度-曲げる角度-親の角度)なら代入しちゃってそうじゃないなら普通に回転させるって感じ
//回転する方向も場合分けする
if (Math.Abs(Math.Atan2(Math.Sin(tag.p.RAD - rkaku - radto), Math.Cos(tag.p.RAD - rkaku - radto))) <= Math.Abs(radsp * 1.1 * t) /*&& (sai||radsp*(tag.p.RAD - rkaku - radto)<=Math.Abs(radsp)*0.01f)*/)
{
double sp = tag.p.RAD - rkaku - radto;
foreach (var a in tags)
{
a.p.RAD -= sp;
}
}
else
{
if (sai)
{
if (Math.Atan2(Math.Sin(tag.p.RAD - rkaku - radto), Math.Cos(tag.p.RAD - rkaku - radto)) < 0)
{
radsp = Math.Abs(radsp);
}
else
{
radsp = -Math.Abs(radsp);
}
}
foreach (var a in tags)
{
a.p.RAD += radsp * t;
}
}
}
}
base.frame(c, cl);
}
}
こんなやつを何個も作ります。サイズ変える奴、透明度変える奴、中心動かす奴、関節の長さ変える奴、テクスチャー変える奴、z変える奴...再生速度も意識しながら書きましょう
エディタを作るぞ!!
キャラクターはヴィジュアルなのでエディタがないといけませんよね。こればっかりは面倒な作業は避けられません。formコントロールを存分に活用しましょう
モーションはコマンドで作らないといけないですから、Microsoft神の御創りに給われたCsharpscriptがありますのでそれを使いましょう。
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;
//中略
try{
work = new motion();//ここで編集するモーションをworkとしてるから上に死ぬほどworkと書かれてるわけですね
string script = scriptbox.Text;
string tmp = script;
work.sp = (float)speedud.Value;//再生するモーションに速度をつける
ScriptOptions a = ScriptOptions.Default
.WithReferences(Assembly.GetEntryAssembly())
.WithImports("System", "System.Collections.Generic", "Charamaker2.Character", "Charamaker2");//これがusingと等価の存在
var Q = CSharpScript.Create(script, options: a, globalsType: typeof(motionmaker));
//globalsTypeは↓でrunner(this)と書いてるように自身のクラスを指定
var runner = Q.CreateDelegate();
var run = (Delegate)runner;
runner(this);//スクリプトを実行するとworkに対してmoveをくっつけられる
sel.addmotion(work);//選択してるキャラクターにモーションをぶち込む
}
catch (Exception ex)
{
exbox.Text += ex.StackTrace + " an " + ex.Message;
}
記憶が定かでないので必要ないのも含まれてるかもしれません。
セーブ方法
データのセーブにはSerializerを使いましょう。あとモーションは書いたスクリプトも同時に保存しとかないと編集ができなきなるので注意です。ロードも同様な感じで。
static public void savemotion(string s, motion m)
{
var saveData = new motionsaveman();
saveData.m = m;
saveData.text = s;
System.Windows.Forms.SaveFileDialog sfd = new SaveFileDialog();//よく見るダイアログを開く
sfd.InitialDirectory = @".\motion";
sfd.FileName = "motion" + DateTime.Now.Year + DateTime.Now.Month + DateTime.Now.Day + "_" + DateTime.Now.Hour + DateTime.Now.Minute + DateTime.Now.Second;
if (sfd.ShowDialog() == DialogResult.OK)
{
//指定したパスにファイルを保存する
Stream fileStream = sfd.OpenFile();
BinaryFormatter bF = new BinaryFormatter();
if (saveData.m != null)
{
bF.Serialize(fileStream, saveData);
Console.WriteLine("save : OK ");
}
fileStream.Close();
}
}
static public motionsaveman loadmotion()
{
motionsaveman res = null;
System.Windows.Forms.OpenFileDialog sfd = new OpenFileDialog();
sfd.InitialDirectory = @".\motion";
sfd.FileName = "motion" + DateTime.Now.Year + DateTime.Now.Month + DateTime.Now.Day + "_" + DateTime.Now.Hour + DateTime.Now.Minute + DateTime.Now.Second;//よくあるダイアログを開く
if (sfd.ShowDialog() == DialogResult.OK)
{
string file = sfd.SafeFileName;
Object loadedData = null;
string dir = sfd.FileName;
//ファイルを読込
try
{
BinaryFormatter binaryFormatter = new BinaryFormatter();
using (var fs = File.OpenRead(dir))
{
loadedData = binaryFormatter.Deserialize(fs);
fs.Close();
}
}
catch { }
res = (motionsaveman)loadedData;
}
return res;
}
おわりに
こんなことやってもキャラクター組み立てたりするのは面倒です。キャラクターを作る方法を作ることなんかより面倒です。私は使ったことありませんがBlenderとかUnityの奴とかのほうが絶対クオリティは高いでしょうし、それを使うことをお勧めします。Live2Dなんかもいいと思います。