Skip to content

Commit

Permalink
Project詳細を表示するページを作成 (#19)
Browse files Browse the repository at this point in the history
* 詳細ページへのリンクを貼る

* ProjectShowsを作成

* FileLoaderを作成

* Loaderの記述場所を変更

* YamlLoaderでFileLoaderを使用するようにする

* ProjectsLoaderをリファクタ

* DataControllerを修正

* dataファイルを修正

* Loaderを修正

* mekashojo.mdを追加

* react-markdownを追加

* Projectを修正

* ファイルパスを修正
  • Loading branch information
piny940 authored Oct 4, 2023
1 parent 64b24fd commit b7433bc
Show file tree
Hide file tree
Showing 12 changed files with 541 additions and 369 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hook-form": "^7.44.3",
"react-markdown": "^8.0.7",
"react-markdown": "^9.0.0",
"sharp": "^0.32.1",
"styled-components": "^5.3.9",
"swr": "^2.1.5"
Expand Down
14 changes: 3 additions & 11 deletions src/components/Portfolio/ProjectItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,6 @@ export const ProjectItem: React.FC<ProjectItemProps> = ({
const projectLink = useMemo(() => project.getLink(), [project])
const qiita = useMemo(() => project.getQiita(), [project])

const renderTitle = () => (
<h3 className="h5 my-1 title-underline pb-1">{project.getTitle()}</h3>
)

return (
<ProjectItemDiv
className={
Expand All @@ -50,13 +46,9 @@ export const ProjectItem: React.FC<ProjectItemProps> = ({
<FavoriteIcon size={42} />
</FavoriteIconDiv>
)}
{qiita ? (
<Link href={qiita} target="_blank">
{renderTitle()}
</Link>
) : (
renderTitle()
)}
<Link href={`/projects/${project.getTitle()}`}>
<h3 className="h5 my-1 title-underline pb-1">{project.getTitle()}</h3>
</Link>
<ul className="list-unstyled mt-2 mb-1 d-flex align-items-center">
{githubLink && (
<li>
Expand Down
29 changes: 29 additions & 0 deletions src/containers/Project.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { PortfolioData } from '@/controllers/data_controller'
import { PortfolioController } from '@/controllers/portfolio_controller'
import Error from 'next/error'
import { useMemo } from 'react'
import ReactMarkdown from 'react-markdown'

export type ProjectShowProps = {
title: string
data: PortfolioData
}

export const ProjectShow: React.FC<ProjectShowProps> = ({ title, data }) => {
const controller = new PortfolioController(data)
const project = useMemo(() => {
const projects = controller.getProjects().getProjects()
return projects.find((project) => project.getTitle() === title)
}, [title])

if (!project) return <Error statusCode={400} />

return (
<div className="wrapper mx-auto mt-3">
<h1 className="title-underline">{project.getTitle()}</h1>
<div className="col-lg-6">
<ReactMarkdown>{project.getDetail()}</ReactMarkdown>
</div>
</div>
)
}
5 changes: 3 additions & 2 deletions src/controllers/data_controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { YamlLoader } from '@/loader/_common'
import { FileLoader, YamlLoader } from '@/loader/common'
import { ProfileLoader } from '@/loader/profile'
import { ProjectsLoader } from '@/loader/projects'
import { TechStacksLoader } from '@/loader/tech_stacks'
Expand Down Expand Up @@ -27,8 +27,9 @@ export class DataController {
}

#load = (): PortfolioData => {
const fileLoader = new FileLoader()
const yamlLoader = new YamlLoader()
const projectsData = new ProjectsLoader(yamlLoader).load()
const projectsData = new ProjectsLoader(fileLoader, yamlLoader).load()
const profileData = new ProfileLoader(yamlLoader).load()
const technologiesData = new TechnologiesLoader(yamlLoader).load()
const techStacksData = new TechStacksLoader(yamlLoader).load()
Expand Down
98 changes: 98 additions & 0 deletions src/data/documents/mekashojo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# 目次
[1. はじめに](#1-はじめに)
[2. 作ったゲームについて](#2-作ったゲームについて)
[3. 開発の流れ](#3-開発の流れ)
[4. プロジェクトを通して学んだこと](#4-プロジェクトを通して学んだこと)

# 1. はじめに
このゲームは私が人生で初めて開発したアプリになります。
制作したのはもう2年も前の話ですが、過去に開発したアプリの記録を残そうと思ってこの記事を書きました。

# 2. 作ったゲームについて
[2-1 ゲームの概要](#2-1-ゲームの概要)
[2-2 ゲームの3つの特徴](#2-2-ゲームの3つの特徴)
[2-3 ゲームの画面](#2-3-ゲームの画面)
[2-4 ゲームの基本操作](#2-4-ゲームの基本操作)

## 2-1 ゲームの概要
作品タイトルは「メカ少女シューティング」で、横スクロール型のシューティングゲームです。少女の姿をしたキャラクターが銃を持って敵を倒していくゲームで、各ステージの最後にはラスボスの敵が配置されています。
![Screenshot 2023_09_26 17_13_28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3330232/940ba456-e119-29df-f260-dc079eba1bf0.png)

## 2-2 ゲームの3つの特徴
**1. アイテムを使用して武器を強化**
敵を倒すと一定確率で武器の強化アイテムが手に入ります。
武器を強化することで、今まではクリアが難しかったステージがクリアできるようになります。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3330232/82484c96-1195-c1d2-0259-caf736f41772.png" width="100" /> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3330232/3a4abf75-b2a9-cebe-d152-c31a7d33b9ec.png" width="100" /> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3330232/d7fe19b6-f945-ce96-aa4e-9298e87eebe1.png" width="100" /> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3330232/59c2fcf4-383e-7c18-eb57-a24097afcaaa.png" width="100" />

**2. 豊富な敵キャラクター**
実装されている敵の種類はモブキャラだけで10体以上!それぞれ固有の攻撃方法を持っています。
![MekashojoShooting 2023_05_21 6_49_07 (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3330232/819746e4-6ce6-a50e-821b-d171e83be3aa.png)

**3. 豊富な種類の武器**
メイン武器、サブ武器、必殺技(ボム)の3種類の攻撃方法に加えて、防御(シールド)を使用することができます。
メイン武器とサブ武器は複数の武器の中からそれぞれ1つ選ぶことができます。![メカ少女 2023_09_26 17_16_44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3330232/c3a5c85b-5ff3-a9a8-f2fa-7f59eb98cf23.png)

## 2-3 ゲームの画面
- スタート画面
NewGameを押すと新規データで、Continueを押すと前回遊んだ続きからプレイできます。
How To Playを押すと遊び方や操作方法が確認できます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3330232/904d724f-d225-69f3-a1d2-b9d96af31bf2.png" width="300" />

- メニュー画面
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3330232/15d36e43-2446-1137-7d06-729202f04d7a.png" width="300" />

- 装備選択画面
バトルで使用する武器を選択・強化できます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3330232/9ce0f337-7ed1-ed1d-4de2-e413b41fe371.png" width="400" />

- ステージ選択画面
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3330232/cbc8e54a-3496-5f98-91a6-ef214772ca75.png" width=300 />

- ゲーム画面
![Screenshot 2023_09_26 17_13_28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3330232/1f9b4619-bf18-abfe-9c9d-b7d86f03b862.png)


## 2-4 ゲームの基本操作
| 名称 | 操作 |
| ---- | ---- |
|上下左右の移動|W + A + S + D|
|武器の使用|左クリック|
|必殺技(ボム)|スペース|
|防御(シールド)|右クリック(長押し)|
|メイン武器とサブ武器の切り替え|マウスホイール|
|一時停止|Escキー|


# 3. 開発の流れ
2021年5月〜11月で要件定義・開発を行いました。

|時期|内容|
|--|--|
|5月|要件定義・仕様書作成|
|6月・7月|開発|
|8月|大規模なリファクタリング|
|9月〜10月|開発|
|11月|最終調整|

プログラマ・グラフィック・音担当の合計10人ほどのチームで開発を行い、私はこのチームのチームリーダーを努めました。具体的には、会議での司会進行・仕様書作成・リファクタリング・コア部分の作成を担当しました。

# 4. プロジェクトを通して学んだこと
**1. オブジェクト指向・MVCの設計**
私はこのゲームを開発した頃まだまだ未熟で、1000行以上ある、いわゆる神クラスを作ってしまったり、Viewとロジックを密結合にしたりしてしまいました。

見かねた上級生の先輩に、コードが読みにくいと指摘され、クラスが密結合だと良くないということだとか、テストが書きやすいコードの書き方だとかを教えてもらい、8月に大規模なリファクタを行い、ロジックとViewが分離されたコードへと修正しました。

この経験のおかげで、RailsのMVCの考え方をすんなり受け入れることができ、この先のプログラミングの勉強にとても役に立ちました。

**2. チーム開発の基礎**
当時私はGitやGithubの使い方を全く知りませんでした。
どれくらい知らなかったかと言うと、
- コミットの単位
- ブランチの切り方
- PRの切り方
- コードレビューの仕方
- Issueの割り振り方

という本当に基本的なことすら分かっていませんでした。

この開発を通して、上級生の人たちにGitの使い方を教えてもらった経験は、この先アルバイトや個人開発でGitを使う際にとても役に立ちました。
3 changes: 2 additions & 1 deletion src/data/projects.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- react
- postgres
isFavorite: true
detailSrc: ''
- title: スタンプ作成補助
id: kokosuko-stamp
description: Youtubeのコメント欄でタイムスタンプを作成するのを補助する拡張機能です
Expand Down Expand Up @@ -65,8 +66,8 @@
id: mekashojo
description: 大学のサークルでシューティングゲームをチームで開発しました。
link: 'https://kmc-jp.booth.pm/items/3438923'
detailSrc: ''
technologies:
- unity
qiita: 'https://qiita.com/piny940/items/d5519acb423ad22c51a8'
isFavorite: true
detailSrc: 'src/data/documents/mekashojo.md'
9 changes: 2 additions & 7 deletions src/loader/_common.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import { readFileSync } from 'fs'
import { load } from 'js-yaml'

export interface IYamlLoader {
load: <T>(filename: string) => T
}

export class YamlLoader implements IYamlLoader {
load = <T>(filename: string) => {
return load(readFileSync(filename, 'utf8')) as T
}
export interface IFileLoader {
load: (filename: string) => string
}
26 changes: 26 additions & 0 deletions src/loader/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { readFileSync } from 'fs'
import { IFileLoader, IYamlLoader } from './_common'
import { load } from 'js-yaml'

export class FileLoader implements IFileLoader {
load = (filename: string) => {
try {
return readFileSync(filename, 'utf8')
} catch {
return ''
}
}
}

export class YamlLoader implements IYamlLoader {
#fileLoader: FileLoader

constructor() {
this.#fileLoader = new FileLoader()
}

load = <T = any>(filename: string): T => {
const content = this.#fileLoader.load(filename)
return load(content) as T
}
}
30 changes: 23 additions & 7 deletions src/loader/projects.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,31 @@
import { ProjectsData } from '@/models/project'
import { IYamlLoader } from './_common'
import { ProjectData, ProjectsData } from '@/models/project'
import { IFileLoader, IYamlLoader } from './_common'

type ProjectsBeforeLoad = Array<Omit<ProjectData, 'detail'>>

export class ProjectsLoader {
#PROJECTS_DATA_PATH = 'src/data/projects.yml'
#loader: IYamlLoader

constructor(loader: IYamlLoader) {
this.#loader = loader
}
constructor(
private readonly fileLoader: IFileLoader,
private readonly yamlLoader: IYamlLoader
) {}

load = () => {
return this.#loader.load<ProjectsData>(this.#PROJECTS_DATA_PATH)
const projectsWithoutDetail = this.yamlLoader.load<ProjectsBeforeLoad>(
this.#PROJECTS_DATA_PATH
)
const projectsData = this.#loadDetails(projectsWithoutDetail)
return projectsData
}

#loadDetails = (projectsBeforeLoad: ProjectsBeforeLoad): ProjectsData => {
const projectsData = projectsBeforeLoad.map((project) => ({
...project,
detail: this.fileLoader.load(project.detailSrc),
}))
console.log(projectsBeforeLoad[0].detailSrc)
console.log(this.fileLoader.load(projectsBeforeLoad[0].detailSrc))
return projectsData
}
}
3 changes: 3 additions & 0 deletions src/models/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export interface ProjectData {
qiita?: string
technologies: string[]
isFavorite: boolean
detailSrc: string
detail: string
}
export type ProjectsData = ProjectData[]

Expand Down Expand Up @@ -41,6 +43,7 @@ export class Project {
}

getIsFavorite = () => this.#data.isFavorite
getDetail = () => this.#data.detail
}

export class Projects {
Expand Down
31 changes: 31 additions & 0 deletions src/pages/projects/[title].tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { ProjectShow } from '@/containers/Project'
import { DataController, PortfolioData } from '@/controllers/data_controller'
import { GetStaticPaths } from 'next'
import { useRouter } from 'next/router'

type ProjectProps = {
data: PortfolioData
}

export const getStaticProps = async (): Promise<{ props: ProjectProps }> => {
const dataController = new DataController()
const data = dataController.getPortfolioData()

return {
props: { data },
}
}
export const getStaticPaths: GetStaticPaths<{ slug: string }> = async () => {
return {
paths: [],
fallback: 'blocking',
}
}

const Project = ({ data }: ProjectProps): JSX.Element => {
const router = useRouter()
const title = router.query.title as string
return <ProjectShow title={title} data={data} />
}

export default Project
Loading

0 comments on commit b7433bc

Please sign in to comment.